<?php
/*
 * Copyright (c) 2023. RTM Business
 */

namespace WordpressModels\ORM\Command;

use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;
use WordpressModels\Serialization\JsonSchemaProps;

class JsonSchemaTool
{

    public function __construct()
    {

    }

    /**
     * Convert a PHP class definition to a JSON Schema using Doctrine and Symfony Serializer components.
     *
     * Can use Serialization Groups to limit the properties that are included in the schema.
     *
     * @param string $className
     * @param array $serializationGroups
     * @return array
     */
    public function toSchema(string $className, array $serializationGroups = []): array
    {
        $reflectionClass = new ReflectionClass($className);
        $properties = $reflectionClass->getProperties();

        $schema = [
            'title' => $reflectionClass->getShortName(),
            'type' => 'object',
            'properties' => [],
            'required' => [],
        ];

        if ($attr = current($reflectionClass->getAttributes(JsonSchemaProps::class))) {
            /** @var JsonSchemaProps $props */
            $props = $attr->newInstance();
            if ($title = $props->title) {
                $schema['title'] = $title;
            }
            if ($description = $props->description) {
                $schema['description'] = $description;
            }
        }


        foreach ($properties as $property) {
            if ($serializationGroups && !$property->getAttributes(Groups::class)) {
                // If we're using serialization groups, skip properties that don't have the @Groups annotation
                continue;
            }

            $propertyName = $property->getName();
            /** @var \ReflectionType $propertyType */
            $propertyType = $property->getType();

            if ($property->getAttributes(Ignore::class)) {
                // Ignore properties with the @Ignore annotation
                continue;
            }

            $schema['properties'][$propertyName] = $this->processProperty($property);

            if (!$propertyType->allowsNull()) {
                $schema['required'][] = $propertyName;
            }
        }

        return $schema;
    }


    /**
     * Determine the JSON Schema type for a given PHP type.
     * @param \ReflectionType $type
     * @return array|string[]
     */
    private function processProperty(\ReflectionProperty $property)
    {
        $type = $property->getType();
        $additionalAttributes = [];
        if ($type instanceof \ReflectionUnionType) {
            $types = array_map([$this, 'determineType'], $type->getTypes());
            $flatTypes = array_map(fn(array $def) => $def['type'], $types);

            return [
                'type' => $flatTypes,
                'anyOf' => $types
            ];
        } elseif ($type instanceof \ReflectionIntersectionType) {
            $types = array_map([$this, 'determineType'], $type->getTypes());
            $flatTypes = array_map(fn(array $def) => $def['type'], $types);

            return [
                'type' => $flatTypes,
                'allOf' => array_map([$this, 'determineType'], $type->getTypes())
            ];
        } elseif ($type instanceof \ReflectionNamedType) {
            $type = $type->getName();

            if (in_array($type, ['int', 'float'])) {
                $t = 'number';
            } elseif ($type === 'bool') {
                $t = 'boolean';
            } elseif ($type === 'array') {
                $t = 'array';
                // todo: items
                // read the reflection docComment for array type
                // generic `array` type will be skipped
                // regex match any of '@var word[]' '@var word|otherWord[]' '@var word&otherWord[]'
                if (preg_match('/@var\s+([\w|&]+)\[\]/', $reflectionProperty->getDocComment(), $matches)) {
                    // some match was found, get the first group (class name)
                    [, $type] = $matches;

                    // omit array [] (E.g. stdClass[] -> stdClass)
                    $arrayClass = substr($type, 0, strlen($type) - 2);


                }
            } elseif ($type === 'string') {
                $t = 'string';
            } elseif ($type === 'object' || class_exists($type)) {
                return $this->toSchema($type);
            } elseif ($type === 'null') {
                $t = 'null';
            } else {
                return [];
            }

            return [
                'type' => $t,
                ...$additionalAttributes
            ];
        }

        return [];
    }
}