<?php

namespace Shm\ShmRPC\ShmRPCCodeGen;

use Shm\ShmTypes\ArrayOfType;
use Shm\ShmTypes\BaseType;
use Shm\ShmTypes\BoolType;
use Shm\ShmTypes\CompositeTypes\FileTypes\FileIDType;
use Shm\ShmTypes\CompositeTypes\VizType\VizType;
use Shm\ShmTypes\EnumType;
use Shm\ShmTypes\FloatType;
use Shm\ShmTypes\IDsType;
use Shm\ShmTypes\IntType;
use Shm\ShmTypes\MixedType;
use Shm\ShmTypes\SelfRefType;
use Shm\ShmTypes\StaticType;
use Shm\ShmTypes\StructureType;
use Shm\ShmTypes\UnixDateTimeType;
use Shm\ShmTypes\UnixDateType;

class JsonSchemaBuilder
{
    /** @var array<string, array<string, mixed>> */
    private static array $components = [];

    /** @var array<string, bool> */
    private static array $building = [];

    public static function reset(): void
    {
        self::$components = [];
        self::$building = [];
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public static function getComponents(): array
    {
        return self::$components;
    }

    public static function addComponent(string $name, array $schema): void
    {
        self::$components[$name] = $schema;
    }

    /**
     * @return array<string, mixed>
     */
    public static function ref(string $name): array
    {
        return ['$ref' => '#/components/schemas/' . $name];
    }

    /**
     * @return array<string, mixed>
     */
    public static function forType(BaseType $type, bool $input = false): array
    {
        if ($type instanceof SelfRefType) {
            return self::forType($type->resolveType(), $input);
        }

        if ($type instanceof FileIDType) {
            return self::forType($type->getDocument(), $input);
        }

        if ($type instanceof StructureType) {
            $name = $input ? $type->inputTypeName() : $type->typeName();
            self::ensureStructureComponent($type, $input, $name);

            return self::ref($name);
        }

        return self::buildInlineSchema($type, $input);
    }

    private static function ensureStructureComponent(StructureType $type, bool $input, string $name): void
    {
        if (isset(self::$components[$name])) {
            return;
        }

        if (isset(self::$building[$name])) {
            self::$components[$name] = ['type' => 'object'];

            return;
        }

        self::$building[$name] = true;
        self::$components[$name] = self::buildStructureSchema($type, $input);
        unset(self::$building[$name]);
    }

    /**
     * @return array<string, mixed>
     */
    private static function buildStructureSchema(StructureType $type, bool $input): array
    {
        $properties = [];
        $required = [];
        $additionalProperties = null;

        foreach ($type->items as $key => $item) {
            if ($item->hide) {
                continue;
            }

            // Если это viz — пропускаем (настройка аналитики, не поле данных)
            if ($item instanceof VizType) {
                continue;
            }

            if ($input && (!$item->editable || $item instanceof StaticType)) {
                continue;
            }

            if ($key === '*') {
                $additionalProperties = self::forType($item, $input);
                continue;
            }

            $propSchema = self::forType($item, $input);
            self::applyFieldMeta($propSchema, $item);
            $properties[$key] = $propSchema;

            if (!$item->nullable) {
                $required[] = $key;
            }
        }

        $schema = [
            'type' => 'object',
            'properties' => $properties,
        ];

        if ($required !== []) {
            $schema['required'] = $required;
        }

        if ($additionalProperties !== null) {
            $schema['additionalProperties'] = $additionalProperties;
        }

        self::applyFieldMeta($schema, $type);

        return $schema;
    }

    /**
     * @return array<string, mixed>
     */
    private static function buildInlineSchema(BaseType $type, bool $input): array
    {
        if ($type instanceof StaticType) {
            $schema = ['const' => $type->normalize(null)];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof EnumType) {
            $schema = [
                'type' => 'string',
                'enum' => array_keys($type->values),
            ];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof ArrayOfType) {
            $schema = [
                'type' => 'array',
                'items' => self::forType($type->itemType, $input),
            ];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof IDsType) {
            $schema = [
                'type' => 'array',
                'items' => ['type' => 'string'],
            ];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof MixedType) {
            $schema = [];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof BoolType) {
            $schema = ['type' => 'boolean'];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof IntType || $type instanceof UnixDateType || $type instanceof UnixDateTimeType) {
            $schema = ['type' => 'integer'];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        if ($type instanceof FloatType) {
            $schema = ['type' => 'number'];
            self::applyFieldMeta($schema, $type);

            return $schema;
        }

        $schema = ['type' => 'string'];
        self::applyFieldMeta($schema, $type);

        return $schema;
    }

    /**
     * @param array<string, mixed> $schema
     */
    private static function applyFieldMeta(array &$schema, BaseType $type): void
    {
        if ($type->title) {
            $schema['description'] = $type->title;
        }

        if ($type->deprecated) {
            $schema['deprecated'] = true;
        }

        if ($type->nullable) {
            $schema['nullable'] = true;
        }
    }
}
