<?php

declare(strict_types=1);

namespace apexl\Io\modules\tabulator\Dto;

use apexl\dto\Dto;
use apexl\entityCore\interfaces\HasCurrencyCastsInterface;
use apexl\Io\includes\System;
use apexl\Io\modules\tabulator\Enums\TabulatorFilterCondition;
use apexl\Io\modules\tabulator\Enums\TabulatorFormatter;
use apexl\Io\modules\tabulator\Enums\TabulatorHeaderFilterType;
use apexl\Io\modules\tabulator\Enums\TabulatorHorizontalAlign;
use apexl\Io\modules\tabulator\Enums\TabulatorMutator;
use apexl\Io\modules\tabulator\Enums\TabulatorVerticalAlign;
use apexl\Io\modules\tabulator\Interfaces\HasTabulatorViewInterface;
use apexl\Io\modules\tabulator\Interfaces\TabulatableColumnCallback;
use apexl\Io\modules\tabulator\Interfaces\TabulatableFilterQueryCallback;
use apexl\Io\modules\tabulator\Interfaces\TabulatableJoinCallback;
use apexl\Io\modules\tabulator\Interfaces\TabulatableLoaderConditionCallback;
use apexl\Io\modules\tabulator\Interfaces\TabulatableSortQueryCallback;
use apexl\Io\modules\tabulator\Interfaces\TabulatorFilterTypeInterface;
use apexl\Io\modules\tabulator\Interfaces\TabulatorRowFormatterInterface;
use apexl\Io\modules\tabulator\Services\TabulatorFieldRenamer;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\ArrayValues;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\Created;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\Date;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\Image;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\ImageMultiple;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\LinkedArrayValues;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\LinkField;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\SimpleField;
use apexl\Io\modules\tabulator\Tabulatable\ColumnCallback\UniqueArrayValues;
use apexl\Io\modules\tabulator\Tabulatable\LoaderConditionCallback\CreatedLoaderConditionCallback;
use JsonSerializable;
use Stringy\Stringy;
use function apexl\Io\assertInstanceOf;
use function apexl\Io\container;

readonly class TabulatorColumnDto extends Dto implements JsonSerializable
{
    public TabulatableColumnCallback $callback;
    public string $title;
    public string $filterOn;
    public string $sortOn;
    public string $field;
    public array $fields;
    public array $dataFields;

    public function __construct(
        string $field,
        array $fields = [],
        array $dataFields = [],
        ?TabulatableColumnCallback $callback = null,
        array $callbackParams = [],
        ?string $title = null,
        public bool $visible = true,
        public bool $headerSort = true,
        public ?TabulatableJoinCallback $join = null,
        public ?TabulatableSortQueryCallback $sortQueryCallback = null,
        public ?TabulatorFilterTypeInterface $headerFilter = null,
        public ?TabulatorFilterCondition $headerFilterFunc = null,
        public array $headerFilterParams = [],
        public ?string $maxWidth = null,
        public ?TabulatorMutator $mutator = null,
        public ?TabulatorFormatter $formatter = null,
        public ?array $formatterParams = null,
        public bool $hiddenTitle = false,
        public ?int $width = null,
        public ?int $minWidth = null,
        public null|bool|string|TabulatorFieldTooltipDto $tooltip = null,
        public ?TabulatableFilterQueryCallback $filterQueryCallback = null,
        public ?TabulatableLoaderConditionCallback $loaderConditionCallback = null,
        public string $hozAlign = 'left',
        public string $vertAlign = 'middle',
        public bool $headerTooltip = true,
        public string $cssClass = '',
        ?string $filterOn = null,
        ?string $sortOn = null,
        public string|null $columnGroupTitle = null,
        public mixed $default = null,
        public ?TabulatorAjaxColumnDefinitionDto $ajax = null,
        public ?TabulatorRowFormatterInterface $rowFormatter = null,
    ) {
        $fieldRenamer = container()->get(TabulatorFieldRenamer::class);
        $fields[] = $field;
        $this->fields = array_unique(array_map(fn(string $field): string => $fieldRenamer->rename($field), $fields));
        $this->field = $fieldRenamer->rename($field);
        $this->dataFields = array_unique(
            array_map(fn(string $field): string => $fieldRenamer->rename($field), $dataFields)
        );

        $this->callback = $this->hydrateCallback($callback ?? SimpleField::class, $callbackParams);

        $this->title = $title ?? Stringy::create($this->field)
            ->humanize()
            ->titleize()
            ->toString();

        $this->filterOn = $filterOn ? $fieldRenamer->rename($filterOn) : $this->field;
        $this->sortOn = $sortOn ? $fieldRenamer->rename($sortOn) : $this->field;
    }

    private function hydrateCallback(
        string|TabulatableColumnCallback|array $callback,
        $params
    ): TabulatableColumnCallback {
        if (is_array($callback)) {
            [$callback, $params] = array_pad($callback, 2, []);
        }

        if (is_string($callback)) {
            assertInstanceOf($callback, TabulatableColumnCallback::class);

            $callback = $params ?
                System::makeRegisteredService($callback, $params) :
                System::getRegisteredService($callback);
        }


        return $callback;
    }

    public static function simpleColumn(string $field, ?string $title = null): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            title: $title,
            formatter: TabulatorFormatter::HTML,
        );
    }

    public static function htmlColumn(string $field, ?string $title = null): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            title: $title,
            formatter: TabulatorFormatter::HTML
        );
    }

    public static function invisibleField(
        string $field,
        string|TabulatableColumnCallback|array|null $callback = null
    ): TabulatorColumnDto {
        $dto = new TabulatorColumnDto(
            field: $field,
            visible: false,
            headerSort: false,
        );

        if ($callback) {
            return $dto->withCallback($callback);
        }

        return $dto;
    }

    public function withCallback(
        string|TabulatableColumnCallback|array $callback,
        array $callbackParams = []
    ): TabulatorColumnDto {
        return $this->with('callback', $this->hydrateCallback($callback, $callbackParams));
    }

    public static function tickColumn(string $field, ?callable $callback = null): TabulatorColumnDto
    {
        return self::tickCrossColumn($field, $callback)->withFormatterParams([
            'tickElement' => '<div class="tickCrossContainer"><i class="fas fa-check-circle text-success"></i></div>',
            'crossElement' => '',
        ]);
    }

    public function withFormatterParams(array $formatterParams): TabulatorColumnDto
    {
        return $this->with('formatterParams', $formatterParams);
    }

    public static function tickCrossColumn(
        string $field,
        null|array|string|TabulatableColumnCallback $callback = null,
        ?string $title = null
    ): TabulatorColumnDto {
        $dto = new TabulatorColumnDto(
            field: $field,
            title: $title,
            formatter: TabulatorFormatter::TICK_CROSS,
            formatterParams: [
                'tickElement' => '<div class="tickCrossContainer"><i class="fas fa-check-circle text-success"></i></div>',
                'crossElement' => '<div class="tickCrossContainer"><i class="fas fa-times-circle text-danger"></i></div>',
            ],
            hozAlign: 'center'
        );

        if ($callback) {
            return $dto->withCallback($callback);
        }

        return $dto;
    }

    public static function crossColumn(string $field, ?callable $callback = null): TabulatorColumnDto
    {
        return self::tickCrossColumn($field, $callback)->withFormatterParams([
            'tickElement' => '',
            'crossElement' => '<div class="tickCrossContainer"><i class="fas fa-times-circle text-danger"></i></div>',
        ]);
    }

    public static function competitorsColumn(string $pathToCompetitors = 'competitors', string $field = '!competitors', string $title = 'Competitors'): TabulatorColumnDto
    {
        return TabulatorColumnDto::linkedArrayColumn(
            sprintf('%s.name', $pathToCompetitors),
            sprintf('%s.view_route', $pathToCompetitors),
            $field
        )
            ->withTitle($title);
    }

    public function withTitle(string $title): TabulatorColumnDto
    {
        return $this->with('title', $title);
    }

    public static function linkedArrayColumn(
        string $labelField,
        string $linkField,
        ?string $field = null,
    ): TabulatorColumnDto {
        $dto = new TabulatorColumnDto(
            field: $field ?? $labelField,
            dataFields: [$labelField, $linkField],
            headerSort: false,
            formatter: TabulatorFormatter::ARRAY,
        );

        return $dto->withCallback([LinkedArrayValues::class, [
            'labelField' => $labelField,
            'linkField' => $linkField
        ]]);
    }

    public static function dateColumn(string $field, ?string $title = null): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            title: $title,
            headerFilter: TabulatorHeaderFilterType::DATE,
            formatter: TabulatorFormatter::DATETIME,
            formatterParams: [
                'outputFormat' => "dd/MM/yyyy",
            ],
        )->withCallback(Date::class);
    }

    public static function dateTimeColumn(string $field, ?string $title = null): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            title: $title,
            headerFilter: TabulatorHeaderFilterType::DATE,
            formatter: TabulatorFormatter::DATETIME,
            formatterParams: [
                'outputFormat' => "dd/MM/yyyy HH:mm:ss",
            ],
            cssClass: 'text-wrap',
        )->withCallback(Date::class);
    }

    public static function imageColumn(
        string $field,
        string $title = '',
        string $maxWidth = '100px',
        ?string $thumbnailField = null,
        ?string $altField = null,
    ): TabulatorColumnDto {
        $fields = array_filter([$thumbnailField, $altField]);
        $dto = (new TabulatorColumnDto(
            field: $field,
            fields: $fields,
            title: $title,
            headerSort: false,
            maxWidth: $maxWidth,
            formatter: TabulatorFormatter::IMAGE,
            hozAlign: 'center',
        ));

        return $dto->withCallback(Image::class, [
            'altField' => $altField,
            'thumbnailField' => $thumbnailField
        ]);
    }

    public static function imageMultipleColumn(
        string $srcField,
        ?string $altField,
        string $title = '',
        array $images = []
    ): TabulatorColumnDto {
        $dto = new TabulatorColumnDto(
            field: $srcField,
            fields: [$altField],
            title: $title,
            headerSort: false,
            formatter: TabulatorFormatter::IMAGE_MULTIPLE,
            formatterParams: [
                'images' => $images,
            ],
        );

        return $dto->withCallback(ImageMultiple::class, ['altField' => str_replace('.', '__', $altField)]);
    }

    public static function linkColumn(string $linkField, ?string $labelField = null, $newWindow = true): TabulatorColumnDto
    {
        $labelField = $labelField ?? $linkField;

        return new TabulatorColumnDto(
            field: $linkField,
            fields: [$labelField],
            formatter: TabulatorFormatter::LINK,
            filterOn: $labelField,
            sortOn: $labelField,
        )->withCallback(LinkField::class, [
            'link' => $linkField,
            'label' => $labelField,
            'newWindow' => $newWindow,
        ]);
    }

    public static function fontAwesomeIconColumn(
        string $field,
        string $title,
        null|string|TabulatableColumnCallback $callback = null
    ): TabulatorColumnDto {
        $dto = new TabulatorColumnDto(
            field: $field,
            title: $title,
            headerSort: false,
            formatter: TabulatorFormatter::FONT_AWESOME_ICON,
            hozAlign: 'center',
        );

        if ($callback) {
            return $dto->withCallback($callback);
        }

        return $dto;
    }

    public static function createdColumn(HasTabulatorViewInterface $entity, string $field = 'created', string $title = 'Created'): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            fields: [
                'creator.name',
                'created',
            ],
            title: $title,
            headerSort: false,
            headerFilter: TabulatorHeaderFilterType::CREATOR,
            headerFilterFunc: TabulatorFilterCondition::CREATOR,
            formatter: TabulatorFormatter::ARRAY,
            formatterParams: [
                'outputFormat' => "dd/MM/yyyy HH:mm:ss",
            ],
        )->withCallback(Created::class)
            ->withLoaderConditionCallback(CreatedLoaderConditionCallback::class, ['entity' => $entity]);
    }

    public function withLoaderConditionCallback(TabulatableLoaderConditionCallback|string $loaderConditionCallback, array $params = []): TabulatorColumnDto
    {
        if (is_string($loaderConditionCallback)) {
            assertInstanceOf($loaderConditionCallback, TabulatableLoaderConditionCallback::class);

            $loaderConditionCallback = System::makeRegisteredService($loaderConditionCallback, $params);
        }


        return $this->with('loaderConditionCallback', $loaderConditionCallback);
    }

    public static function modifiedColumn(
        HasTabulatorViewInterface $entity,
        string $dateField = 'modified',
        string $contactField = 'modifier.name',
        string $title = 'Modified'
    ): TabulatorColumnDto {
        return new TabulatorColumnDto(
            field: $dateField,
            fields: [
                $contactField,
                $dateField,
            ],
            title: $title,
            headerSort: false,
            headerFilter: TabulatorHeaderFilterType::CREATOR,
            headerFilterFunc: TabulatorFilterCondition::CREATOR,
            formatter: TabulatorFormatter::ARRAY,
            formatterParams: [
                'outputFormat' => "dd/MM/yyyy HH:mm:ss",
            ],
        )->withCallback(Created::class, [
            'dateField' => $dateField,
            'contactField' => $contactField,
        ])->withLoaderConditionCallback(CreatedLoaderConditionCallback::class, [
            'entity' => $entity,
            'dateField' => $dateField,
            'contactField' => $contactField,
        ]);
    }

    public static function dateDueColumn(string $field): TabulatorColumnDto
    {
        return new TabulatorColumnDto(
            field: $field,
            formatter: TabulatorFormatter::DATE_DUE,
            hozAlign: TabulatorHorizontalAlign::CENTER->value,
        )->withCallback(Date::class);
    }

    public static function moneyColumn(string $field, ?HasCurrencyCastsInterface $hasCurrencyCasts, ?string $title = null, bool $zeroIsBlank = true): TabulatorColumnDto
    {
        $dto = new TabulatorColumnDto(
            field: $field,
            title: $title,
            headerFilter: TabulatorHeaderFilterType::MIN_MAX_MONEY,
            formatterParams: [
                'zeroIsBlank' => $zeroIsBlank,
            ],
        );

        if ($currencyCodeField = $hasCurrencyCasts->currencyCodeField()) {
            $dto = $dto->withDataFields($currencyCodeField);
        }

        return $dto;
    }

    public function withDataFields(string|array $fields): TabulatorColumnDto
    {
        return $this->with('dataFields', (array) $fields);
    }

    public static function arrayColumn(
        string $field,
        string|TabulatableColumnCallback|array|null $fieldsOrCallback = null,
        bool $unique = false,
    ): TabulatorColumnDto {
        $fields = [];

        if (is_null($fieldsOrCallback) || is_array($fieldsOrCallback)) {
            $fields = $fieldsOrCallback ?? [$field];
            $fieldsOrCallback = [$unique ? UniqueArrayValues::class : ArrayValues::class, ['fields' => $fields]];
        }

        $dto = new TabulatorColumnDto(
            field: $field,
            fields: $fields,
            headerSort: false,
            formatter: TabulatorFormatter::ARRAY,
        );


        if ($fieldsOrCallback) {
            return $dto->withCallback($fieldsOrCallback);
        }

        return $dto;
    }

    public function withFilterQueryCallback(TabulatableFilterQueryCallback|string $filterQueryCallback): TabulatorColumnDto
    {
        if (is_string($filterQueryCallback)) {
            assertInstanceOf($filterQueryCallback, TabulatableFilterQueryCallback::class);
            $filterQueryCallback = System::getRegisteredService($filterQueryCallback);
        }


        return $this->with('filterQueryCallback', $filterQueryCallback);
    }

    public function withFormatter(TabulatorFormatter $formatter, ?array $formatterParams = []): TabulatorColumnDto
    {
        if ($formatter === TabulatorFormatter::PERCENTAGE) {
            $formatter = TabulatorFormatter::NUMBER;

            $formatterParams['numberFormatterOptions'] = [
                ...$formatterParams['numberFormatterOptions'] ?? [],
                'style' => 'percent',
            ];
        }

        if ($formatter === TabulatorFormatter::NUMBER) {
            if (isset($formatterParams['digits'])) {
                $formatterParams['numberFormatterOptions'] = $formatterParams['numberFormatterOptions'] ?? [];
                $formatterParams['numberFormatterOptions']['minimumFractionDigits'] = $formatterParams['numberFormatterOptions']['minimumFractionDigits'] ?? $formatterParams['digits'];
                $formatterParams['numberFormatterOptions']['maximumFractionDigits'] = $formatterParams['numberFormatterOptions']['maximumFractionDigits'] ?? $formatterParams['digits'];
            }
        }

        $dto = $this->with('formatter', $formatter);

        return $formatterParams ? $dto->withFormatterParams($formatterParams) : $dto;
    }

    public function notSortable(): TabulatorColumnDto
    {
        return $this->withHeaderSort(false);
    }

    public function withHeaderSort(bool $headerSort = true): TabulatorColumnDto
    {
        return $this->with('headerSort', $headerSort);
    }

    public function withHeaderFilter(
        TabulatorFilterTypeInterface $headerFilter = TabulatorHeaderFilterType::INPUT,
        array $headerFilterParams = []
    ): TabulatorColumnDto {
        if ($headerFilter === TabulatorHeaderFilterType::TICK_CROSS) {
            $headerFilterParams['tristate'] = $headerFilterParams['tristate'] ?? true;
        }

        if ($headerFilter === TabulatorHeaderFilterType::SELECT) {
            // Select is deprecated by Tabulator
            $headerFilter = TabulatorHeaderFilterType::LIST;
        }

        if ($headerFilter === TabulatorHeaderFilterType::LIST) {
            $headerFilterParams['clearable'] = $headerFilterParams['clearable'] ?? true;
        }


        $dto = $this
            ->with('headerFilter', $headerFilter)
            ->with('headerFilterParams', $headerFilterParams);

        if ($headerFilter === TabulatorHeaderFilterType::LIST && $dto->headerFilterFunc === null) {
            return $dto->withHeaderFilterFunc(TabulatorFilterCondition::EXACT);
        }


        return $dto;
    }

    public function withHeaderFilterFunc(TabulatorFilterCondition $func): TabulatorColumnDto
    {
        return $this->with('headerFilterFunc', $func);
    }

    public function withSortQueryCallback(string|TabulatableSortQueryCallback $callback): TabulatorColumnDto
    {
        if (is_string($callback)) {
            assertInstanceOf($callback, TabulatableSortQueryCallback::class);

            $callback = System::makeRegisteredService($callback);
        }

        return $this->with('sortQueryCallback', $callback);
    }

    public function withWidth(int $width): TabulatorColumnDto
    {
        return $this->with('width', $width);
    }

    public function withMinWidth(int $minWidth): TabulatorColumnDto
    {
        return $this->with('minWidth', $minWidth);
    }

    public function withHiddenTitle(bool $hiddenTitle = true): TabulatorColumnDto
    {
        return $this->with('hiddenTitle', $hiddenTitle);
    }

    public function withTooltip(bool|string|TabulatorFieldTooltipDto $tooltip = true): TabulatorColumnDto
    {
        return $this->with('tooltip', $tooltip);
    }

    public function withJoin(string|TabulatableJoinCallback $joinCallback): TabulatorColumnDto
    {
        if (is_string($joinCallback)) {
            assertInstanceOf($joinCallback, TabulatableJoinCallback::class);

            $joinCallback = System::getRegisteredService($joinCallback);
        }

        return $this->with('join', $joinCallback);
    }

    public function withMutator(TabulatorMutator $mutator): TabulatorColumnDto
    {
        return $this->with('mutator', $mutator);
    }

    public function jsonSerialize(): array
    {
        $data = [
            'ajax' => $this->ajax,
            'field' => $this->field,
            'title' => $this->title,
            'visible' => $this->visible,
            'headerSort' => $this->headerSort,
            'headerFilter' => $this->headerFilter,
            'headerFilterFunc' => $this->headerFilterFunc,
            'headerFilterParams' => $this->headerFilterParams,
            'hiddenTitle' => $this->hiddenTitle,
            'maxWidth' => $this->maxWidth,
            'mutator' => $this->mutator,
            'formatter' => $this->formatter,
            'formatterParams' => $this->formatterParams,
            'width' => $this->width,
            'tooltip' => $this->tooltip,
            'vertAlign' => $this->vertAlign,
            'hozAlign' => $this->hozAlign,
            'headerTooltip' => $this->headerTooltip,
            'cssClass' => $this->cssClass,
            'columnGroupTitle' => $this->columnGroupTitle,
            'rowFormatter' => $this->rowFormatter,
        ];

        if ($this->minWidth !== null) {
            $data['minWidth'] = $this->minWidth;
        }

        return $data;
    }

    public function centered(): TabulatorColumnDto
    {
        return $this->withHozAlign(TabulatorHorizontalAlign::CENTER);
    }

    public function withHozAlign(TabulatorHorizontalAlign $hozAlign): TabulatorColumnDto
    {
        return $this->with('hozAlign', $hozAlign->value);
    }

    public function withVertAlign(TabulatorVerticalAlign $vertAlign): TabulatorColumnDto
    {
        return $this->with('vertAlign', $vertAlign->value);
    }

    public function withFilterOn(string $string): TabulatorColumnDto
    {
        return $this->with('filterOn', str_replace('.', '__', $string));
    }

    public function withColumnGroup(TabulatorColumnGroup $columnGroup): TabulatorColumnDto
    {
        return $this->with('columnGroupTitle', $columnGroup->title)->withCssClass(
            'tabulator-column-group-' . $columnGroup->id
        );
    }

    public function withCssClass(string $cssClass): TabulatorColumnDto
    {
        return $this->with('cssClass', $cssClass);
    }

    public function withFields(array $fields): TabulatorColumnDto
    {
        return $this->with('fields', $fields);
    }

    public function withDefault(mixed $default): TabulatorColumnDto
    {
        return $this->with('default', $default);
    }

    public function withAjax(
        string $primaryKey = 'id',
        array $additionalFields = []
    ): TabulatorColumnDto {
        return $this->with(
            'ajax',
            TabulatorAjaxColumnDefinitionDto::create(...func_get_args())
        );
    }

    /**
     * @param class-string<TabulatorRowFormatterInterface> $formatter
     */
    public function withRowFormatter(string $formatter, array $params = []): TabulatorColumnDto
    {
        assertInstanceOf($formatter, TabulatorRowFormatterInterface::class);

        return $this->with('rowFormatter', new $formatter($params));
    }

    protected function protect(): array
    {
        return [];
    }
}
