<?php

declare(strict_types=1);

namespace Apexl\Io\Dtos;

use Apexl\Io\Enums\Casts;
use stdClass;

/**
 * One table's shape, derived once from its description.
 *
 * Everything the entity needs to know about its own columns comes from here: the raw
 * description rows, the field names, and the typed EntityField list. Before this existed the
 * same DESCRIBE was reached through four methods and memoised in six different places, at
 * three different scopes, which is how an entity could end up believing it had no fields at
 * all.
 */
final readonly class EntitySchema
{
    /** @var array<string, true> */
    private array $lookup;

    /**
     * @param array<string, stdClass> $columns description rows, keyed by column name
     * @param EntityField[] $fields
     */
    private function __construct(
        public array $columns,
        public array $fields,
    ) {
        $this->lookup = array_fill_keys(array_keys($columns), true);
    }

    /** @param array<string, stdClass> $columns */
    public static function fromColumns(array $columns): self
    {
        return new self($columns, array_values(array_map(self::toField(...), $columns)));
    }

    public function has(string $field): bool
    {
        return isset($this->lookup[$field]);
    }

    /** @return string[] */
    public function names(): array
    {
        return array_keys($this->columns);
    }

    /**
     * The columns an entity is allowed to write, i.e. everything but the read-only ones.
     *
     * @param string[] $readOnly
     * @return array<string, stdClass>
     */
    public function writableColumns(array $readOnly): array
    {
        return array_diff_key($this->columns, array_fill_keys($readOnly, true));
    }

    private static function toField(stdClass $column): EntityField
    {
        $type = $column->Type;
        $length = null;

        if (preg_match('/^([a-z]+)(?:\(([\d,]+)\))?/', (string) $type, $match)) {
            [, $type, $length] = array_pad($match, 3, null);
        }

        $type = strtolower((string) $type);

        return new EntityField(
            $column->Field,
            match ($type) {
                'date' => Casts::DATE,
                'datetime' => Casts::DATETIME,
                'int', 'smallint', 'mediumint', 'bigint' => Casts::INT,
                'tinyint' => Casts::BOOL,
                'float', 'decimal' => Casts::FLOAT,
                default => Casts::STRING,
            },
            $type,
            strtoupper((string) $column->Null) === 'NO',
            $length ?? null,
        );
    }
}
