<?php
declare( strict_types=1 );

namespace RankingCoach\Inc\Core;

if ( !defined('ABSPATH') ) {
    exit;
}

use BeyondSEODeps\DDD\Infrastructure\Reflection\ReflectionUnionType;
use RankingCoach\Inc\Core\Base\BaseConstants;
use RankingCoach\Inc\Core\Helpers\Attributes\RcDocumentation;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionProperty;

/**
 * Class OpenApiGenerator
 */
class OpenApiGenerator {
	private array $openapi;

	/**
	 * OpenApiGenerator constructor.
	 * @throws ReflectionException
	 */
	public function __construct()
	{
		$this->initializeOpenApi();
		$this->generatePaths();
	}

	/**
	 * Initializes the OpenAPI object.
	 */
	private function initializeOpenApi(): void
	{
		$this->openapi = [
			'openapi' => '3.0.0',
			'info' => [
				'title' => 'WordPress REST API',
				'version' => get_option(BaseConstants::OPTION_API_VERSION, '1.0.0'),
				'description' => 'API Generated by SEO Plugin',
			],
			'servers' => [
				['url' => get_rest_url()],
			],
			'paths' => [],
			'components' => [
				'schemas' => [],
			],
		];
	}

	/**
	 * Generates the paths for the OpenAPI object.
	 * @throws ReflectionException
	 */
	private function generatePaths(): void
	{
		$routes = rest_get_server()->get_routes();

		foreach ($routes as $route => $handlers) {
			if (!$this->isValidRoute($route)) {
				continue;
			}

			if (!is_array($handlers)) {
				continue;
			}

			$formattedRoute = $this->formatRoute($route);

			foreach ($handlers as $handler) {
				if ($this->isValidHandler($handler)) {
					$this->processHandler($formattedRoute, $handler);
				}
			}
		}
	}

	/**
	 * @param string $route
	 *
	 * @return bool
	 */
	private function isValidRoute(string $route): bool
	{
		return str_contains($route, RANKINGCOACH_REST_API_BASE) && $route !== '/' . RANKINGCOACH_REST_API_BASE;
	}

	/**
	 * @param string $route
	 *
	 * @return string
	 */
	private function formatRoute(string $route): string
	{
		return preg_replace('/\(\?P<(\w+)>[^)]+\)/', '{$1}', $route);
	}

	/**
	 * @param array $handler
	 *
	 * @return bool
	 */
	private function isValidHandler(array $handler): bool
	{
		return !empty($handler['methods']) && !empty($handler['callback']);
	}

	/**
	 * @param string $formattedRoute
	 * @param array $handler
	 *
	 * @throws ReflectionException
	 */
	private function processHandler(string $formattedRoute, array $handler): void
	{
		$methods = array_map(fn($a) => strtolower($a), array_keys($handler['methods']));
		$pathItem = $this->openapi['paths'][$formattedRoute] ?? [];


		foreach ($methods as $method) {
			$pathItem[$method] = $this->createPathItem($method, $handler);
		}

		$this->openapi['paths'][$formattedRoute] = $pathItem;
	}

	/**
	 * @param string $method
	 * @param array $handler
	 *
	 * @return array
	 * @throws ReflectionException
	 */
	private function createPathItem(string $method, array $handler): array
	{
		[$summary, $description, $request, $response] = $this->extractDocumentationFromAttribute($handler['callback']);

		$parameters = $this->extractParameters($handler['args'] ?? [], $method);

		$schemaProperties = [
			'properties' => [],
			'required' => [],
		];

		$requestSchemaEntity = null;
		if ($request) {
			$fullNamespace = $this->getNamespaceFromClass($request, $schemaProperties);
			$requestSchemaEntity = str_replace("\\", '.', $fullNamespace);

			// Add schema entity to components
			$this->openapi['components']['schemas'][$requestSchemaEntity] = [
				'type' => 'object',
				'properties' => $schemaProperties['properties'],
				'required' => $schemaProperties['required'],
			];
		}

		$schemaProperties = [
			'properties' => [],
			'required' => [],
		];

		$responseSchemaEntity = null;
		if($response) {
			$fullNamespace = $this->getNamespaceFromClass($response, $schemaProperties);
			$responseSchemaEntity = str_replace("\\", '.', $fullNamespace);

			$this->openapi['components']['schemas'][$responseSchemaEntity] = [
				'type' => 'object',
				'properties' => $schemaProperties['properties'],
				'required' => $schemaProperties['required'],
			];
		}


		$pathItem = [
			'summary' => $summary,
			'description' => $description,
			'parameters' => $parameters,
			'responses' => [
				'200' => [
					'description' => 'Successful response'
				],
			]
		];

		if($responseSchemaEntity && !empty($this->openapi['components']['schemas'][$responseSchemaEntity])) {
			$pathItem['responses']['200']['content']['application/json']['schema']['$ref'] = '#/components/schemas/' . $responseSchemaEntity;
		}

		if (in_array(strtolower($method), ['post', 'put', 'patch', 'delete'])) {
			$pathItem['requestBody'] = $this->createRequestBody($handler['args'] ?? [], $requestSchemaEntity ?? null);
		}

		return $pathItem;
	}

	/**
	 * Extracts documentation from RcDocumentation attribute.
	 *
	 * @param callable|array $callback
	 *
	 * @return array
	 */
	private function extractDocumentationFromAttribute(callable|array $callback): array
	{
		$summary = '';
		$description = '';
		$request = null;
		$response = null;

		try {
			$reflection = is_array($callback)
				? new ReflectionMethod($callback[0], $callback[1])
				: new ReflectionFunction($callback);

			$attributes = $reflection->getAttributes(RcDocumentation::class);

			if (!empty($attributes)) {
				/* @var RcDocumentation $rcDocumentation */
				$rcDocumentation = $attributes[0]->newInstance();
				$summary = $rcDocumentation->getSummary();
				$description = $rcDocumentation->getDescription();
				$request = $rcDocumentation->getRequestDto();
				$response = $rcDocumentation->getResponseDto();
				// response is something like that: PluginInformationResponseDto::class
			}
		} catch (ReflectionException) {
			// do nothing
		}

		return [$summary, $description, $request, $response];
	}

	/**
	 * @param array $args
	 * @param string $method
	 *
	 * @return array
	 */
	private function extractParameters(array $args, string $method): array
	{
		$parameters = [];
		foreach ($args as $argName => $argDetails) {
			if (!empty($argDetails['in']) && $argDetails['in'] !== 'body') {
				$parameters[] = [
					'name' => $argName,
					'description' => $argDetails['description'] ?? '',
					'required' => $argDetails['required'] ?? false,
					'in' => $argDetails['in'],
					'schema' => [
						'type' => $argDetails['type'] ?? 'string',
					],
				];
			}
		}
		return $parameters;
	}

	/**
	 * @param array $args
	 * @param string|null $schemaEntity
	 *
	 * @return array
	 */
	private function createRequestBody(array $args, ?string $schemaEntity = null): array
	{
		$schema = [
			'type' => 'object',
			'properties' => [],
			'required' => [],
		];

		foreach ($args as $argName => $argDetails) {
			if (!empty($argDetails['in']) && $argDetails['in'] === 'body') {
				$schema['properties'][$argName] = [
					'type' => $argDetails['type'] ?? 'string',
					'description' => $argDetails['description'] ?? '',
				];
				if (!empty($argDetails['required'])) {
					$schema['required'][] = $argName;
				}
			}
		}

		return [
			'content' => [
				'application/json' => [
					'schema' => $schemaEntity ? [
						'$ref' => '#/components/schemas/' . str_replace("\\", '.', $schemaEntity),
					] : $schema,
				],
			],
		];
	}

	/**
	 * @return array
	 */
	public function getOpenApi(): array
	{
		return $this->openapi;
	}

	/**
	 * @param mixed $requestClass
	 * @param array $classProperties
	 *
	 * @return string
	 * @throws ReflectionException
	 */
	private function getNamespaceFromClass(mixed $requestClass, array &$classProperties = []): string {
		$reflection = new ReflectionClass($requestClass);
		$docComment = $reflection->getDocComment();
		$phpDocProperties = $this->parsePhpDocProperties($docComment);

		foreach ($reflection->getProperties(ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC) as $property) {
			// Skip inherited properties except "objectType"
			if (
				$property->getDeclaringClass()->getName() !== $reflection->getName()
				&& $property->getName() !== 'objectType'
				&& $property->getName() !== 'elements'
			) {
				continue;
			}

			$propertyName = $property->getName();
			$type = $property->getType();

			if(!$type) {
				continue;
			}

			$nullable = $type->allowsNull();
			$isNamedType = $type instanceof ReflectionNamedType;
			$isUnionType = $type instanceof ReflectionUnionType;

			$typeName = $isNamedType ? $type->getName() : null;
			// Handle $typeName for union types
			if ($isUnionType) {
				$unionTypes = $type->getTypes();
				$typeName = $unionTypes[0]->getName();
			}

			$isBuiltInType = $isNamedType ? $type->isBuiltin() : ($isUnionType ? false : null);
			if($isBuiltInType && in_array($typeName, ['int', 'bool', 'float', 'string'])) {
				$typeName = $this->mapType($typeName);
			}

			$defaultValue = $property->hasDefaultValue() ? $property->getDefaultValue() : null;
			$description = $this->extractPropertyDescription($property);


//			// Handle union types
//			$unionTypes = $type->getTypes() ?? [];
//			if (!empty($unionTypes)) {
//	            $typeName = $unionTypes[0]->getName();
//	            $classProperties['properties'][$propertyName] = [
//                    'type' => $this->mapType($typeName),
//                    'default' => $defaultValue,
//                    'nullable' => $nullable,
//                    'description' => $description,
//                ];
//	            continue;
//            }

			// Handle complex types (non-built-in)
			if (isset($phpDocProperties[$propertyName]) && str_ends_with($phpDocProperties[$propertyName], '[]')) {
				$elementType = rtrim($phpDocProperties[$propertyName], '[]');

				// Handle object arrays
				if (!in_array($elementType, ['string', 'int', 'bool', 'float', 'array', 'object'])) {
					$refClassName = str_replace("\\", '.', $elementType);

					if (!isset($this->openapi['components']['schemas'][$refClassName])) {
						$this->openapi['components']['schemas'][$refClassName] = ['type' => 'object', 'properties' => [], 'required' => []];
						$this->getNamespaceFromClass($elementType, $this->openapi['components']['schemas'][$refClassName]);
					}

					$classProperties['properties'][$propertyName] = [
						'type' => 'array',
						'default' => $defaultValue,
						'nullable' => $nullable,
						'items' => [
							'$ref' => '#/components/schemas/' . $refClassName,
						],
						'description' => $description,
					];
				} else {
					// Handle primitive arrays
					$classProperties['properties'][$propertyName] = [
						'type' => 'array',
						'default' => [],
						'nullable' => $nullable,
						'items' => [
							'type' => $this->mapType($elementType),
						],
						'description' => $description,
					];
				}
			} elseif ($typeName && !$type->isBuiltin()) {
				// Map and handle referenced class
				$refClass = new ReflectionClass($typeName);
				$refClassName = str_replace("\\", '.', $typeName);

				// Check if property is in PHPDoc with array type
				if (isset($phpDocProperties[$propertyName]) && str_ends_with($phpDocProperties[$propertyName], '[]')) {
					$elementType = rtrim($phpDocProperties[$propertyName], '[]');
				}

				// Recursively generate schema for referenced class if not already processed
				if (!isset($this->openapi['components']['schemas'][$refClassName])) {
					// Initialize schema first
					$this->openapi['components']['schemas'][$refClassName] = ['type' => 'object', 'properties' => [], 'required' => []];

					// Populate schema recursively
					$this->getNamespaceFromClass($refClass->getName(), $this->openapi['components']['schemas'][$refClassName]);
				}

				// Reference the resolved schema
				$classProperties['properties'][$propertyName] = [
					'$ref' => '#/components/schemas/' . $refClassName,
				];
			} else {
				// Handle primitive types
				$classProperties['properties'][$propertyName] = [
					'type' => $this->mapType($typeName),
					'default' => $defaultValue,
					'nullable' => $nullable,
					'description' => $description,
				];
			}

			// Add property to required list
			($nullable ? null : ($classProperties['required'][] = $propertyName));
		}

		$className = $reflection->getShortName();
		return $reflection->getNamespaceName() . '\\' . $className;
	}

	/**
	 * Parses PHPDoc to extract property definitions.
	 *
	 * @param string|null $docComment
	 *
	 * @return array
	 * @noinspection RegExpSimplifiable*/
	private function parsePhpDocProperties(?string $docComment): array {
		$properties = [];

		if ($docComment && preg_match_all('/@property\s+([^\s]+)\s+\$?(\w+)/', $docComment, $matches, PREG_SET_ORDER)) {
			foreach ($matches as $match) {
				$type = $match[1];
				$name = $match[2];
				$properties[$name] = $type;
			}
		}

		return $properties;
	}

	/**
	 * @param ReflectionProperty $property
	 *
	 * @return string
	 * @noinspection RegExpRedundantEscape
	 */
	private function extractPropertyDescription(ReflectionProperty $property): string {
		$docComment = $property->getDocComment();
		if (!$docComment) {
			return '';
		}

		// Try to get full description after @var (including multi-line)
		if (preg_match('/\@var\s+[^\n]+\n\s*\*\s*(.*?)(?=\n\s*\*\/)/s', $docComment, $matches)) {
			return trim(preg_replace('/\s*\*\s*/', ' ', $matches[1]));
		}

		// Try to get inline description after @var
		if (preg_match('/@var\s+[\\\\\w\[\]|]+\s+(.*?)(?=\n|\*\/)/m', $docComment, $matches)) {
			return trim($matches[1]);
		}

		// Try to get description before any @ tags
		if (preg_match('/\/\*\*\s*\n\s*\*\s*(.*?)(?=\n\s*\*\s*@|\n\s*\*\/)/s', $docComment, $matches)) {
			return trim(preg_replace('/\s*\*\s*/', ' ', $matches[1]));
		}

		return '';
	}

	/**
	 * Maps PHP types to OpenAPI types.
	 *
	 * @param string $typeName
	 * @param bool $namedType
	 *
	 * @return string
	 */
	private function mapType(string $typeName, bool $namedType = false): string {
		if($namedType) {
			return $typeName;
		}
		return match ($typeName) {
			'int' => 'integer',
			'bool' => 'boolean',
			'float', 'double' => 'number',
			'array' => 'array',
			'object' => 'object',
			'string' => 'string',
			default => 'string',
		};
	}
}
