<?php

declare(strict_types=1);

namespace apexl\Io\factories;

use apexl\entityCore\collections\EntityCollection;
use apexl\Io\includes\Entity;
use apexl\Io\interfaces\HasEntityFactory;
use apexl\Utils\Strings\Camel;
use Faker\Factory as FakerFactory;
use Faker\Generator;
use function apexl\Io\assertInstanceOf;

readonly class EntityFactory
{
    protected Generator $faker;

    public function __construct(protected string $entityClass)
    {
        assertInstanceOf($this->entityClass, Entity::class);
        assertInstanceOf($this->entityClass, HasEntityFactory::class);

        $this->faker = FakerFactory::create();
    }

    public function create(array $attrs = [], bool $store = true): Entity
    {
        /**
         * @var Entity $entity
         */
        $entity = new ($this->entityClass)();

        $combinedAttrs = [
            ...$this->baseDefinition($entity),
            ...$this->definition(),
            ...$attrs,
        ];

        foreach ($combinedAttrs as $property => $value) {
            $entity->{$property} = $value;
        }

        if ($store) {
            $entity->store(false);
        }

        return $entity;
    }

    protected function baseDefinition(Entity $entity): array
    {
        $output = [];
        foreach ($entity->entityFields() as $field) {
            if (in_array($field->field, [$entity->getPrimaryKey()])) {
                continue;
            }

            $value = match ($field->field) {
                'created', 'modified' => $this->faker->unixTime(),
                'created_by', 'modified_by' => $this->faker->randomNumber(),
                default => match ($field->sqlType) {
                    'varchar', 'char' => $this->faker->word(),
                    'date' => $this->faker->date(),
                    'datetime' => $this->faker->dateTime(),
                    'int', 'smallint', 'mediumint', 'bigint' => $this->faker->numberBetween(
                        0,
                        $this->maxIntValue($field->sqlType)
                    ),
                    'float', 'decimal' => $this->faker->randomFloat(
                        explode(',', $field->length)[1],
                        0,
                        str_pad(
                            '',
                            (int) explode(',', $field->length)[0] - (int) explode(',', $field->length)[1],
                            '9'
                        )
                    ),
                    'text', 'mediumtext', 'longtext', 'tinytext' => $this->faker->text(),
                    'tinyint' => $this->faker->boolean(),
                    default => null,
                }
            };

            if ($field->length !== null) {
                $type = gettype($value);
                $value = substr((string) $value, 0, (int) $field->length);
                settype($value, $type);
            }

            $output[$field->field] = $value;
        }

        return $output;
    }

    private function maxIntValue(string $type): int
    {
        return match ($type) {
            'smallint' => 32767,
            'mediumint' => 8388607,
            'int' => 2147483647,
            'bigint' => 9223372036854775807,
        };
    }

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

    protected static function newClassBasename(array $entityClass): string
    {
        return Camel::camelCase(array_pop($entityClass), true) . 'Factory';
    }

    public function createMany(int $count, array $attrs = [], bool $store = true): EntityCollection
    {
        $entities = new EntityCollection();

        for ($i = 0; $i < $count; $i++) {
            $entities->add($this->create($attrs, $store));
        }

        return $entities;
    }
}
