<?php

namespace Filament\Support\Commands\Concerns;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;

trait CanReadModelSchemas
{
    /**
     * @return class-string<Model>|null
     */
    protected function parseModel(string $model): ?string
    {
        if (! class_exists($model)) {
            return null;
        }

        return $model;
    }

    /**
     * @param  class-string<Model>  $model
     */
    protected function getModelSchema(string $model): Builder
    {
        return app($model)
            ->getConnection()
            ->getSchemaBuilder();
    }

    /**
     * @param  class-string<Model>  $model
     */
    protected function getModelTable(string $model): string
    {
        return app($model)->getTable();
    }

    /**
     * @param  class-string<Model>  $model
     */
    protected function guessBelongsToRelationshipName(string $column, string $model): ?string
    {
        /** @var Model $modelInstance */
        $modelInstance = app($model);
        $modelInstanceReflection = new ReflectionClass($modelInstance);
        $guessedRelationshipName = str($column)->beforeLast('_id');
        $hasRelationship = $modelInstanceReflection->hasMethod($guessedRelationshipName);

        if (! $hasRelationship) {
            $guessedRelationshipName = $guessedRelationshipName->camel();
            $hasRelationship = $modelInstanceReflection->hasMethod($guessedRelationshipName);
        }

        if (! $hasRelationship) {
            return null;
        }

        try {
            $type = $modelInstanceReflection->getMethod($guessedRelationshipName)->getReturnType();

            if (
                (! $type) ||
                (! method_exists($type, 'getName')) ||
                ($type->getName() !== BelongsTo::class)
            ) {
                return null;
            }
        } catch (ReflectionException $exception) {
            return null;
        }

        return $guessedRelationshipName;
    }

    protected function guessBelongsToRelationshipTableName(string $column): ?string
    {
        $tableName = str($column)->beforeLast('_id');

        if (Schema::hasTable(Str::plural($tableName))) {
            return Str::plural($tableName);
        }

        if (! Schema::hasTable($tableName)) {
            return null;
        }

        return $tableName;
    }

    /**
     * @param  class-string<Model>  $model
     */
    protected function guessBelongsToRelationshipTitleColumnName(string $column, string $model): string
    {
        $schema = $this->getModelSchema($model);
        $table = $this->getModelTable($model);

        $columns = collect($schema->getColumnListing($table));

        if ($columns->contains('name')) {
            return 'name';
        }

        if ($columns->contains('title')) {
            return 'title';
        }

        return collect($schema->getIndexes($table))
            ->firstWhere('primary')['columns'][0] ?? 'id';
    }

    /**
     * @param  array<string, mixed>  $column
     * @return array<string, mixed>
     */
    protected function parseColumnType(array $column): array
    {
        $type = match ($column['type']) {
            'tinyint(1)', 'bit' => 'boolean',
            'varchar(max)', 'nvarchar(max)' => 'text',
            default => null,
        };

        $type ??= match ($column['type_name']) {
            'boolean', 'bool' => 'boolean',
            'char', 'bpchar', 'nchar' => 'char',
            'varchar', 'nvarchar' => 'string',
            'integer', 'int', 'int4', 'tinyint', 'smallint', 'int2', 'mediumint', 'bigint', 'int8' => 'integer',
            'decimal', 'numeric' => 'decimal',
            'float', 'real', 'float4' => 'float',
            'double', 'float8' => 'double',
            'money', 'smallmoney' => 'money',
            'date' => 'date',
            'time', 'timetz' => 'time',
            'datetime', 'datetime2', 'smalldatetime', 'datetimeoffset' => 'datetime',
            'timestamp', 'timestamptz' => 'timestamp',
            'text', 'tinytext', 'longtext', 'mediumtext', 'ntext' => 'text',
            'json', 'jsonb' => 'json',
            'binary', 'varbinary', 'blob', 'tinyblob', 'mediumblob', 'longblob', 'bytea' => 'binary',
            'geometry', 'geography', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection', 'geomcollection' => 'binary',
            default => $column['type_name'],
        };

        $values = str_contains($column['type'], '(')
            ? str_getcsv(Str::between($column['type'], '(', ')'), enclosure: "'", escape: '\\')
            : null;

        $values = is_null($values) ? [] : match ($type) {
            'string', 'char', 'binary', 'bit' => ['length' => (int) $values[0]],
            'enum' => ['values' => array_map(fn (string $value): string => trim($value, '\''), $values)],
            default => [],
        };

        return array_merge(['name' => $type], array_filter($values));
    }

    /**
     * Columns skipped while reading the model schema, keyed by column name with their database type as the value.
     *
     * @var array<string, string>
     */
    protected array $skippedColumns = [];

    /**
     * @param  array<string, mixed>  $type
     */
    protected function canGenerateSchemaComponentForColumnType(array $type): bool
    {
        // Binary and spatial columns (`blob`, `geometry`, `point`, etc.) hold data that
        // is not valid UTF-8, so it cannot be serialized to JavaScript for a schema component.
        return $type['name'] !== 'binary';
    }

    /**
     * @param  class-string<Model>  $model
     * @param  array<string, mixed>  $column
     */
    protected function recordSkippedColumn(string $model, array $column): void
    {
        // A column that is already `$hidden` is excluded from the model's array representation, so it cannot cause errors and does not need a warning.
        if (in_array($column['name'], app($model)->getHidden(), strict: true)) {
            return;
        }

        $this->skippedColumns[$column['name']] = $column['type_name'];
    }

    /**
     * @return array<string, string>
     */
    public function getSkippedColumns(): array
    {
        return $this->skippedColumns;
    }

    /**
     * @param  array<string, mixed>  $column
     * @param  class-string<Model>  $model
     */
    protected function parseDefaultExpression(array $column, string $model): mixed
    {
        $default = $column['default'];

        if (blank($default)) {
            return null;
        }

        $driver = app($model)->getConnection()->getDriverName();

        if (in_array($driver, ['mysql', 'mariadb'])) {
            if ($default === 'NULL'
            || preg_match("/^\(.*\)$/", $default) === 1
            || str_ends_with($default, '()')
            || str_starts_with(strtolower($default), 'current_timestamp')) {
                return null;
            }

            if (preg_match("/^'(.*)'$/", $default, $matches) === 1) {
                return str_replace("''", "'", $matches[1]);
            }
        }

        if ($driver === 'pgsql') {
            if (str_starts_with($default, 'NULL::')) {
                $default = null;
            }

            if (preg_match("/^['(](.*)[')]::/", $default, $matches) === 1) {
                return str_replace("''", "'", $matches[1]);
            }
        }

        if ($driver === 'sqlsrv') {
            while (preg_match('/^\((.*)\)$/', $default, $matches)) {
                $default = $matches[1];
            }

            if ($default === 'NULL'
            || str_ends_with($default, '()')) {
                return null;
            }

            if (preg_match('/^\'(.*)\'$/', $default, $matches) === 1) {
                return str_replace("''", "'", $matches[1]);
            }
        }

        if ($driver === 'sqlite') {
            if ($default === 'NULL'
            || str_starts_with(strtolower($default), 'current_timestamp')) {
                return null;
            }

            if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
                return str_replace("''", "'", $matches[1]);
            }
        }

        return $default;
    }

    public function getRecordTitleAttribute(): ?string
    {
        return null;
    }

    /**
     * @param  class-string<Model>  $model
     * @return array<string>
     */
    protected function getEnumCasts(string $model): array
    {
        $casts = app($model)->getCasts();

        return array_filter(
            $casts,
            fn (mixed $cast): bool => is_string($cast) ? enum_exists($cast) : false,
        );
    }
}
