<?php

namespace Filament\Actions\Concerns;

use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use Closure;
use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Enums\Contracts\ExportFormat as ExportFormatInterface;
use Filament\Actions\Exports\Enums\ExportFormat;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Jobs\CreateXlsxFile;
use Filament\Actions\Exports\Jobs\ExportCompletion;
use Filament\Actions\Exports\Jobs\PrepareCsvExport;
use Filament\Actions\Exports\Models\Export;
use Filament\Forms;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Split;
use Filament\Notifications\Notification;
use Filament\Support\Facades\FilamentIcon;
use Filament\Tables\Actions\ExportAction as ExportTableAction;
use Filament\Tables\Actions\ExportBulkAction as ExportTableBulkAction;
use Filament\Tables\Contracts\HasTable;
use Illuminate\Bus\PendingBatch;
use Illuminate\Foundation\Bus\PendingChain;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use Livewire\Component;

trait CanExportRecords
{
    /**
     * @var class-string<Exporter>
     */
    protected string $exporter;

    protected ?string $job = null;

    protected int | Closure $chunkSize = 100;

    protected int | Closure | null $maxRows = null;

    protected string | Closure | null $csvDelimiter = null;

    /**
     * @var array<string, mixed> | Closure
     */
    protected array | Closure $options = [];

    protected string | Closure | null $fileDisk = null;

    protected string | Closure | null $fileName = null;

    /**
     * @var array<ExportFormatInterface> | Closure | null
     */
    protected array | Closure | null $formats = null;

    protected ?Closure $modifyQueryUsing = null;

    protected bool | Closure $hasColumnMapping = true;

    protected function setUp(): void
    {
        parent::setUp();

        $this->label(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): string => __('filament-actions::export.label', ['label' => $action->getPluralModelLabel()]));

        $this->modalHeading(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): string => __('filament-actions::export.modal.heading', ['label' => $action->getPluralModelLabel()]));

        $this->modalSubmitActionLabel(__('filament-actions::export.modal.actions.export.label'));

        $this->groupedIcon(FilamentIcon::resolve('actions::export-action.grouped') ?? 'heroicon-m-arrow-down-tray');

        $this->form(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): array => [
            ...($action->hasColumnMapping() ? [Fieldset::make(__('filament-actions::export.modal.form.columns.label'))
                ->columns(1)
                ->inlineLabel()
                ->schema(function () use ($action): array {
                    return array_map(
                        fn (ExportColumn $column): Split => Split::make([
                            Forms\Components\Checkbox::make('isEnabled')
                                ->label(__('filament-actions::export.modal.form.columns.form.is_enabled.label', ['column' => $column->getName()]))
                                ->hiddenLabel()
                                ->default($column->isEnabledByDefault())
                                ->live()
                                ->grow(false),
                            Forms\Components\TextInput::make('label')
                                ->label(__('filament-actions::export.modal.form.columns.form.label.label', ['column' => $column->getName()]))
                                ->hiddenLabel()
                                ->default($column->getLabel())
                                ->placeholder($column->getLabel())
                                ->disabled(fn (Forms\Get $get): bool => ! $get('isEnabled'))
                                ->required(fn (Forms\Get $get): bool => (bool) $get('isEnabled')),
                        ])
                            ->verticallyAlignCenter()
                            ->statePath($column->getName()),
                        $action->getExporter()::getColumns(),
                    );
                })
                ->statePath('columnMap')] : []),
            ...$action->getExporter()::getOptionsFormComponents(),
        ]);

        $this->action(function (ExportAction | ExportTableAction | ExportTableBulkAction $action, array $data, Component $livewire) {
            $exporter = $action->getExporter();

            if ($livewire instanceof HasTable) {
                $query = $livewire->getTableQueryForExport();
            } else {
                $query = $exporter::getModel()::query();
            }

            $query = $exporter::modifyQuery($query);

            $options = array_merge(
                $action->getOptions(),
                Arr::except($data, ['columnMap']),
            );

            if ($this->modifyQueryUsing) {
                $query = $this->evaluate($this->modifyQueryUsing, [
                    'query' => $query,
                    'options' => $options,
                ]) ?? $query;
            }

            $records = $action instanceof ExportTableBulkAction ? $action->getRecords() : null;

            $totalRows = $records ? $records->count() : $query->toBase()->getCountForPagination();

            if ((! $records) && $query->getQuery()->limit) {
                $totalRows = min($totalRows, $query->getQuery()->limit);
            }

            $maxRows = $action->getMaxRows() ?? $totalRows;

            if ($maxRows < $totalRows) {
                Notification::make()
                    ->title(__('filament-actions::export.notifications.max_rows.title'))
                    ->body(trans_choice('filament-actions::export.notifications.max_rows.body', $maxRows, [
                        'count' => Number::format($maxRows),
                    ]))
                    ->danger()
                    ->send();

                return;
            }

            $user = auth()->user();

            if ($action->hasColumnMapping()) {
                $columnMap = collect($data['columnMap'])
                    ->dot()
                    ->reduce(fn (Collection $carry, mixed $value, string $key): Collection => $carry->mergeRecursive([
                        Str::beforeLast($key, '.') => [Str::afterLast($key, '.') => $value],
                    ]), collect())
                    ->filter(fn (array $column): bool => $column['isEnabled'] ?? false)
                    ->mapWithKeys(fn (array $column, string $columnName): array => [$columnName => $column['label']])
                    ->all();
            } else {
                $columnMap = collect($exporter::getColumns())
                    ->mapWithKeys(fn (ExportColumn $column): array => [$column->getName() => $column->getLabel()])
                    ->all();
            }

            $export = app(Export::class);
            $export->user()->associate($user);
            $export->exporter = $exporter;
            $export->total_rows = $totalRows;

            $exporter = $export->getExporter(
                columnMap: $columnMap,
                options: $options,
            );

            $export->file_disk = $action->getFileDisk() ?? $exporter->getFileDisk();
            // Temporary save to obtain the sequence number of the export file.
            $export->save();

            // Delete the export directory to prevent data contamination from previous exports with the same ID.
            $export->deleteFileDirectory();

            $export->file_name = $action->getFileName($export) ?? $exporter->getFileName($export);
            $export->save();

            $formats = $action->getFormats() ?? $exporter->getFormats();
            $hasCsv = in_array(ExportFormat::Csv, $formats);
            $hasXlsx = in_array(ExportFormat::Xlsx, $formats);

            $serializedQuery = EloquentSerializeFacade::serialize($query);

            $job = $action->getJob();
            $jobQueue = $exporter->getJobQueue();
            $jobConnection = $exporter->getJobConnection();
            $jobBatchName = $exporter->getJobBatchName();

            // We do not want to send the loaded user relationship to the queue in job payloads,
            // in case it contains attributes that are not serializable, such as binary columns.
            $export->unsetRelation('user');

            $makeCreateXlsxFileJob = fn (): CreateXlsxFile => app(CreateXlsxFile::class, [
                'export' => $export,
                'columnMap' => $columnMap,
                'options' => $options,
            ]);

            Bus::chain([
                Bus::batch([app($job, [
                    'export' => $export,
                    'query' => $serializedQuery,
                    'columnMap' => $columnMap,
                    'options' => $options,
                    'chunkSize' => $action->getChunkSize(),
                    'records' => $action instanceof ExportTableBulkAction ? $action->getRecords()->all() : null,
                ])])
                    ->when(
                        filled($jobQueue),
                        fn (PendingBatch $batch) => $batch->onQueue($jobQueue),
                    )
                    ->when(
                        filled($jobConnection),
                        fn (PendingBatch $batch) => $batch->onConnection($jobConnection),
                    )
                    ->when(
                        filled($jobBatchName),
                        fn (PendingBatch $batch) => $batch->name($jobBatchName),
                    )
                    ->allowFailures(),
                ...(($hasXlsx && (! $hasCsv)) ? [$makeCreateXlsxFileJob()] : []),
                app(ExportCompletion::class, [
                    'export' => $export,
                    'columnMap' => $columnMap,
                    'formats' => $formats,
                    'options' => $options,
                ]),
                ...(($hasXlsx && $hasCsv) ? [$makeCreateXlsxFileJob()] : []),
            ])
                ->when(
                    filled($jobQueue),
                    fn (PendingChain $chain) => $chain->onQueue($jobQueue),
                )
                ->when(
                    filled($jobConnection),
                    fn (PendingChain $chain) => $chain->onConnection($jobConnection),
                )
                ->dispatch();

            if (
                (filled($jobConnection) && ($jobConnection !== 'sync')) ||
                (blank($jobConnection) && (config('queue.default') !== 'sync'))
            ) {
                Notification::make()
                    ->title($action->getSuccessNotificationTitle())
                    ->body(trans_choice('filament-actions::export.notifications.started.body', $export->total_rows, [
                        'count' => Number::format($export->total_rows),
                    ]))
                    ->success()
                    ->send();
            }
        });

        $this->defaultColor('gray');

        $this->modalWidth('xl');

        $this->successNotificationTitle(__('filament-actions::export.notifications.started.title'));

        if (! $this instanceof ExportTableBulkAction) {
            $this->model(fn (ExportAction | ExportTableAction $action): string => $action->getExporter()::getModel());
        }
    }

    public static function getDefaultName(): ?string
    {
        return 'export';
    }

    /**
     * @param  class-string<Exporter>  $exporter
     */
    public function exporter(string $exporter): static
    {
        $this->exporter = $exporter;

        return $this;
    }

    /**
     * @param  class-string | null  $job
     */
    public function job(?string $job): static
    {
        $this->job = $job;

        return $this;
    }

    public function chunkSize(int | Closure $size): static
    {
        $this->chunkSize = $size;

        return $this;
    }

    public function maxRows(int | Closure | null $rows): static
    {
        $this->maxRows = $rows;

        return $this;
    }

    public function csvDelimiter(string | Closure | null $delimiter): static
    {
        $this->csvDelimiter = $delimiter;

        return $this;
    }

    /**
     * @return class-string<Exporter>
     */
    public function getExporter(): string
    {
        return $this->exporter;
    }

    /**
     * @return class-string
     */
    public function getJob(): string
    {
        return $this->job ?? PrepareCsvExport::class;
    }

    public function getChunkSize(): int
    {
        return $this->evaluate($this->chunkSize);
    }

    public function getMaxRows(): ?int
    {
        return $this->evaluate($this->maxRows);
    }

    /**
     * @param  array<string, mixed> | Closure  $options
     */
    public function options(array | Closure $options): static
    {
        $this->options = $options;

        return $this;
    }

    /**
     * @return array<string, mixed>
     */
    public function getOptions(): array
    {
        return $this->evaluate($this->options);
    }

    public function fileDisk(string | Closure | null $disk): static
    {
        $this->fileDisk = $disk;

        return $this;
    }

    public function getFileDisk(): ?string
    {
        return $this->evaluate($this->fileDisk);
    }

    public function fileName(string | Closure | null $name): static
    {
        $this->fileName = $name;

        return $this;
    }

    public function getFileName(Export $export): ?string
    {
        return $this->evaluate($this->fileName, [
            'export' => $export,
        ]);
    }

    /**
     * @param  array<ExportFormatInterface> | Closure | null  $formats
     */
    public function formats(array | Closure | null $formats): static
    {
        $this->formats = $formats;

        return $this;
    }

    /**
     * @return array<ExportFormatInterface> | null
     */
    public function getFormats(): ?array
    {
        return $this->evaluate($this->formats);
    }

    public function modifyQueryUsing(?Closure $callback): static
    {
        $this->modifyQueryUsing = $callback;

        return $this;
    }

    public function columnMapping(bool | Closure $condition = true): static
    {
        $this->hasColumnMapping = $condition;

        return $this;
    }

    public function hasColumnMapping(): bool
    {
        return (bool) $this->evaluate($this->hasColumnMapping);
    }
}
