<?php

declare(strict_types=1);

namespace App\Filament\Concerns;

use BadMethodCallException;
use Filament\Actions\Action;
use Filament\Actions\AssociateAction;
use Filament\Actions\AttachAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\DetachAction;
use Filament\Actions\DetachBulkAction;
use Filament\Actions\DissociateAction;
use Filament\Actions\DissociateBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Commands\FileGenerators\Concerns\CanGenerateModelTables;
use Filament\Tables\Filters\BaseFilter;
use Filament\Tables\Filters\TrashedFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Nette\PhpGenerator\Literal;
use ReflectionClass;

trait CanGenerateResourceTables
{
    use CanGenerateModelTables;

    /**
     * Get custom table components from the specified directory.
     */
    protected function getTableComponents(): array
    {
        $components = [];
        $tableComponentsPath = app_path('Filament/Components/Table');

        if (! File::exists($tableComponentsPath)) {
            return [];
        }

        $files = File::files($tableComponentsPath);

        foreach ($files as $file) {
            $className = 'App\\Filament\\Components\\Table\\' . Str::before($file->getFilename(), '.php');
            if (! class_exists($className)) {
                continue;
            }

            $reflection = new ReflectionClass($className);
            if ($reflection->isAbstract()) {
                continue;
            }

            $componentName = $reflection->getShortName();
            $types = ['TextColumn', 'ImageColumn', 'IconColumn'];
            foreach ($types as $type) {
                $componentName = str_replace($type, '', $componentName);
            }
            $columnName = Str::snake($componentName);
            $components[$columnName] = [
                'type' => '\\' . $className,
                'constructor' => 'create',
                'methods' => [],
                'component_type' => 'custom',
            ];
        }

        return $components;
    }

    /**
     * @param  ?class-string<Model>  $model
     * @param  array<string>  $exceptColumns
     */
    public function generateTableMethodBody(?string $model = null, array $exceptColumns = []): string
    {
        $this->importUnlessPartial(BulkActionGroup::class);

        $recordTitleAttributeOutput = '';

        if (filled($recordTitleAttribute = $this->getRecordTitleAttribute())) {
            $recordTitleAttributeOutput = new Literal(<<<'PHP'

                ->recordTitleAttribute(?)
            PHP, [$recordTitleAttribute]);
        }

        if (filled($headerActionsOutput = $this->outputTableHeaderActions())) {
            $headerActionsOutput = <<<PHP

                ->headerActions([
                    {$headerActionsOutput}
                ])
            PHP;
        }

        $modifyQueryOutput = '';

        if ($this->isSoftDeletable() && $this->hasTableModifyQueryForSoftDeletes()) {
            $this->namespace->addUse(Builder::class);
            $this->namespace->addUse(SoftDeletingScope::class);

            $modifyQueryOutput = <<<PHP

                ->modifyQueryUsing(fn ({$this->simplifyFqn(Builder::class)} \$query) => \$query
                    ->withoutGlobalScopes([
                        {$this->simplifyFqn(SoftDeletingScope::class)}::class,
                    ]))
            PHP;
        }

        return <<<PHP
            return \$table{$recordTitleAttributeOutput}
                ->columns([
                    {$this->outputTableColumns($model, $exceptColumns)}
                ])
                ->filters([
                    {$this->outputTableFilters()}
                ]){$headerActionsOutput}
                ->recordActions([
                    {$this->outputTableActions()}
                ])
                ->toolbarActions([
                    {$this->simplifyFqn(BulkActionGroup::class)}::make([
                        {$this->outputTableMethodBulkActions()}
                    ]),
                ]){$modifyQueryOutput};
            PHP;
    }

    /**
     * @param  ?class-string<Model>  $model
     * @param  array<string>  $exceptColumns
     */
    public function outputTableColumns(?string $model = null, array $exceptColumns = []): string
    {
        if (! method_exists($this, 'getModelFqn')) {
            throw new BadMethodCallException('Method getModelFqn is not defined in ' . get_class($this));
        }

        $model = $this->getModelFqn($model);

        if (blank($model) || ! class_exists($model)) {
            $recordTitleAttribute = $this->getRecordTitleAttribute();

            if (blank($recordTitleAttribute)) {
                return '//';
            }

            $this->importUnlessPartial(TextColumn::class);

            return new Literal(<<<'PHP'
                \Filament\Tables\Columns\TextColumn::make(?)
                            ->searchable(),
                PHP, [$recordTitleAttribute]);
        }

        $schema = $this->getModelSchema($model);
        $table = $this->getModelTable($model);
        $tableComponents = $this->getTableComponents();

        $columns = [];

        foreach ($schema->getColumns($table) as $column) {
            $columnName = $column['name'];

            if (in_array($columnName, $exceptColumns) || $column['auto_increment']) {
                continue;
            }

            $type = $this->parseColumnType($column);

            if (in_array($type['name'], ['json', 'text']) || str($columnName)->endsWith(['_token']) || str($columnName)->contains(['password'])) {
                continue;
            }

            $columnData = [];

            // Check for custom component first
            if (isset($tableComponents[$columnName])) {
                $customComponent = $tableComponents[$columnName];
                $columnData['type'] = $customComponent['type'];
                $columnData[$customComponent['constructor']] = [];
                $columnData['component_type'] = $customComponent['component_type'] ?? 'custom';
                foreach ($customComponent['methods'] as $method => $params) {
                    $columnData[$method] = $params;
                }
            } elseif (str($columnName)->endsWith('_id') || str($columnName)->endsWith('_by')) {
                // Handle relationships
                if (str($columnName)->endsWith('_id')) {
                    $standardRelationName = explode('_', $columnName)[0];
                } else {
                    $standardRelationName = $columnName;
                }

                if (isset($tableComponents[$standardRelationName])) {
                    $customComponent = $tableComponents[$standardRelationName];
                    $columnName = $standardRelationName;
                    $columnData['type'] = $customComponent['type'];
                    $columnData['create'] = 'create';
                    $columnData['component_type'] = 'custom';
                } else {
                    $guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
                    if (filled($guessedRelationshipName)) {
                        $guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
                        $columnName = "{$guessedRelationshipName}.{$guessedRelationshipTitleColumnName}";
                    }
                    $columnData['type'] = TextColumn::class;
                    $columnData['searchable'] = [];
                }
            } else {
                // Default column configuration
                if (in_array($columnName, ['id', 'sku', 'uuid'])) {
                    $columnData['label'] = [Str::upper($columnName)];
                }

                if ($type['name'] === 'boolean') {
                    $columnData['type'] = IconColumn::class;
                    $columnData['boolean'] = [];
                } else {
                    $columnData['type'] = match (true) {
                        $columnName === 'image' || str($columnName)->startsWith('image_') || str($columnName)->contains('_image_') || str($columnName)->endsWith('_image') => ImageColumn::class,
                        default => TextColumn::class,
                    };

                    if (in_array($type['name'], ['string', 'char']) && $columnData['type'] === TextColumn::class) {
                        $columnData['searchable'] = [];
                    }

                    if ($type['name'] === 'date') {
                        $columnData['date'] = [];
                        $columnData['sortable'] = [];
                    }

                    if (in_array($type['name'], ['datetime', 'timestamp'])) {
                        $columnData['dateTime'] = [];
                        $columnData['sortable'] = [];
                    }

                    if (in_array($type['name'], ['integer', 'decimal', 'float', 'double', 'money'])) {
                        $columnData[in_array($columnName, ['cost', 'money', 'price']) || $type['name'] === 'money' ? 'money' : 'numeric'] = [];
                        $columnData['sortable'] = [];
                    }
                }

                if (in_array($columnName, ['created_at', 'updated_at', 'deleted_at'])) {
                    $standardRelationName = explode('_', $columnName)[0];
                    if (isset($tableComponents[$standardRelationName])) {
                        $customComponent = $tableComponents[$standardRelationName];
                        $columnName = $standardRelationName;
                        $columnData['type'] = $customComponent['type'];
                        $columnData['create'] = 'create';
                        $columnData['component_type'] = 'custom';
                        unset($columnData['dateTime'], $columnData['sortable']);
                    } else {
                        $columnData['toggleable'] = ['isToggledHiddenByDefault' => true];
                    }
                }
            }

            // Generate column output
            $output = '';

            if (! isset($columnData['component_type'])) {
                $output .= '\\' . $columnData['type'];
            } else {
                $output .= (string) str($columnData['type']);
            }

            if (isset($columnData['create'])) {
                if ($columnName === 'status') {
                    $output .= '::create(' . class_basename($model) . '::class)';
                } else {
                    $output .= '::create()';
                }
            } else {
                $output .= '::make(\'' . $columnName . '\')';
            }

            unset($columnData['type'], $columnData['create'], $columnData['component_type']);

            foreach ($columnData as $methodName => $parameters) {
                $output .= '->' . $methodName . '(';
                $output .= collect($parameters)
                    ->map(function (mixed $parameterValue, int|string $parameterName): string {
                        $parameterValue = match (true) {
                            is_bool($parameterValue) => $parameterValue ? 'true' : 'false',
                            is_null($parameterValue) => 'null',
                            is_numeric($parameterValue) => $parameterValue,
                            default => "'{$parameterValue}'",
                        };

                        if (is_numeric($parameterName)) {
                            return $parameterValue;
                        }

                        return "{$parameterName}: {$parameterValue}";
                    })
                    ->implode(', ');
                $output .= ')';
            }

            $output .= ',';

            $columns[$columnName] = $output;
        }

        if (empty($columns)) {
            return '//';
        }

        return implode(PHP_EOL . '        ', $columns);
    }

    /**
     * @return array<class-string<BaseFilter>>
     */
    public function getTableFilters(): array
    {
        $filters = [];

        if ($this->isSoftDeletable()) {
            $filters[] = TrashedFilter::class;
        }

        foreach ($filters as $filter) {
            $this->importUnlessPartial($filter);
        }

        return $filters;
    }

    public function outputTableFilters(): string
    {
        $filters = $this->getTableFilters();

        if (empty($filters)) {
            return '//';
        }

        return implode(PHP_EOL . '        ', array_map(
            fn (string $filter) => "{$this->simplifyFqn($filter)}::make(),",
            $filters,
        ));
    }

    /**
     * @return array<class-string<Action>>
     */
    public function getTableHeaderActions(): array
    {
        $actions = [];

        if ($this->hasCreateTableAction()) {
            $actions[] = CreateAction::class;
        }

        if ($this->hasAttachTableActions()) {
            $actions[] = AttachAction::class;
        }

        if ($this->hasAssociateTableActions()) {
            $actions[] = AssociateAction::class;
        }

        foreach ($actions as $action) {
            $this->importUnlessPartial($action);
        }

        if ($this->hasPartialImports()) {
            $this->namespace->addUse('Filament\Actions');
        }

        return $actions;
    }

    public function outputTableHeaderActions(): ?string
    {
        $actions = $this->getTableHeaderActions();

        if (empty($actions)) {
            return null;
        }

        return implode(PHP_EOL . '        ', array_map(
            fn (string $action) => "{$this->simplifyFqn($action)}::make(),",
            $actions,
        ));
    }

    /**
     * @return array<class-string<Action>>
     */
    public function getTableActions(): array
    {
        $actions = [];

        if ($this->hasViewOperation()) {
            $actions[] = ViewAction::class;
        }

        $actions[] = EditAction::class;

        if ($this->hasAssociateTableActions()) {
            $actions[] = DissociateAction::class;
        }

        if ($this->hasAttachTableActions()) {
            $actions[] = DetachAction::class;
        }

        if ($this->hasDeleteTableActions()) {
            $actions[] = DeleteAction::class;

            if ($this->isSoftDeletable()) {
                $actions[] = ForceDeleteAction::class;
                $actions[] = RestoreAction::class;
            }
        }

        foreach ($actions as $action) {
            $this->importUnlessPartial($action);
        }

        return $actions;
    }

    public function hasCreateTableAction(): bool
    {
        return false;
    }

    public function hasAssociateTableActions(): bool
    {
        return false;
    }

    public function hasAttachTableActions(): bool
    {
        return false;
    }

    public function hasDeleteTableActions(): bool
    {
        return $this->isSimple();
    }

    public function outputTableActions(): string
    {
        return implode(PHP_EOL . '        ', array_map(
            fn (string $action) => "{$this->simplifyFqn($action)}::make(),",
            $this->getTableActions(),
        ));
    }

    /**
     * @return array<class-string<Action>>
     */
    public function getTableBulkActions(): array
    {
        $actions = [];

        if ($this->hasAssociateTableActions()) {
            $actions[] = DissociateBulkAction::class;
        }

        if ($this->hasAttachTableActions()) {
            $actions[] = DetachBulkAction::class;
        }

        $actions[] = DeleteBulkAction::class;

        if ($this->isSoftDeletable()) {
            $actions[] = ForceDeleteBulkAction::class;
            $actions[] = RestoreBulkAction::class;
        }

        foreach ($actions as $action) {
            $this->importUnlessPartial($action);
        }

        return $actions;
    }

    public function outputTableMethodBulkActions(): string
    {
        return implode(PHP_EOL . '            ', array_map(
            fn (string $action) => "{$this->simplifyFqn($action)}::make(),",
            $this->getTableBulkActions(),
        ));
    }

    public function hasTableModifyQueryForSoftDeletes(): bool
    {
        return false;
    }
}
