<?php

declare(strict_types=1);

namespace mirzaev\huesos\models;

// Files of the project
use mirzaev\huesos\models\core,
	mirzaev\huesos\models\traits\document as document_trait,
	mirzaev\huesos\models\interfaces\document as document_interface,
	mirzaev\huesos\models\interfaces\collection as collection_interface;

// Library for ArangoDB
use ArangoDBClient\Document as _document;

// Framework for ArangoDB
use mirzaev\arangodb\collection,
	mirzaev\arangodb\document,
	mirzaev\arangodb\enumerations\collection\type;

// Built-in libraries
use exception;

/**
 * Model of entry
 *
 * @package mirzaev\huesos\models
 *
 * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
 * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
 */
final class entry extends core implements document_interface, collection_interface
{
	use document_trait;

	/**
	 * Name of the collection in ArangoDB
	 */
	final public const string COLLECTION = 'entry';

	/**
	 * Type of the collection in ArangoDB
	 */
	public const type TYPE = type::edge;

	/**
	 * Write an entry 
	 *
	 * @param category|product $from Descendant document
	 * @param category $to Ascendant document
	 * @param array &$errors Registry of errors
	 *
	 * @return string|null Identifier (_id) of instance of the entry document in ArangoDB, if created
	 */
	public static function write(
		category|product $from,
		category $to,
		array &$errors = []
	): string|null {
		try {
			if (collection::initialize($from::COLLECTION, $from::TYPE, errors: $errors)) {
				if (collection::initialize($to::COLLECTION, $to::TYPE, errors: $errors)) {
					if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
						// Initialized collections

						// Creating the entry and exit (success)
						return document::write(
							static::COLLECTION,
							[
								'_from' => $from->getId(),
								'_to' => $to->getId(),
								'version' => ROBOT_VERSION ?? '0.0.0'
							],
							errors: $errors
						);
					} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
				} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
			} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
		} catch (exception $e) {
			// Writing to the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Ascendants
	 *
	 * Search for ascendants that are not descendants for anyone
	 *
	 * @param category|product $descendant Descendant document
	 * @param string|null $return Return (AQL)
	 * @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
	 * @param array &$errors Registry of errors
	 *
	 * @return array|null Ascendants that are not descendants for anyone, if found
	 */
	public static function ascendants(
		category|product $descendant,
		?string $return = 'DISTINCT ascendant',
		array $parameters = [],
		array &$errors = []
	): ?array {
		try {
			if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
				// Initialized the collection
					
			// Search for ascendants
				if ($result = collection::execute(
					sprintf(
						<<<'AQL'
						FOR d IN @@collection
							FOR ascendant IN OUTBOUND d @@edge
								RETURN %s
						AQL,
						empty($return) ? 'DISTINCT ascendant' : $return
					),
					[
						'@collection' => $descendant::COLLECTION,
						'@edge' => static::COLLECTION
					] + $parameters,
					errors: $errors
				)) {
					// Found ascendants

					// Exit (success)
					return is_array($result) ? $result : [$result];
				} else return [];
			} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
		} catch (exception $e) {
			// Writing to the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}


	/**	
	 * Check existence of entry between documents
	 *
	 * @param category|product $from Descendant document
	 * @param category $to Ascendant document
	 * @param array &$errors Registry of errors
	 *
	 * @return ?_document The entry edge, if found
	 */
	public static function check(
		category|product $from,
		category $to,
		array &$errors = []
	): ?_document {
		try {
			if (collection::initialize($from::COLLECTION, $from::TYPE, errors: $errors)) {
				if (collection::initialize($to::COLLECTION, $to::TYPE, errors: $errors)) {
					if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
						// Initialized collections

						if ($entry = collection::execute(
							<<<'AQL'
							FOR d IN @@collection
								FILTER d._from == @from && d._to == @to
								SORT d.updated DESC, d.created DESC
								LIMIT 1
								RETURN d
							AQL,
							[
								'@collection' => static::COLLECTION,
								'from' => $from->getId(),
								'to' => $to->getId()
							],
							errors: $errors
						)) {
							// Found the entry between $from and $to

							// Exit (success)
							return is_array($entry) ? $entry[0] : $entry;
						} else return null;
					} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
				} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
			} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
		} catch (exception $e) {
			// Writing to the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Поиск вхождений (подкатегории или товары)
	 *
	 * Находит вхождения через ребро entry
	 * Генерирует _type со значениями "category" и "product"
	 * относительно того есть ли у документа ещё вложения 
	 * (подразумевается, что у product вложений быть не может)
	 * Объединяет возвращаемые объекты документа с переменной _type
	 *
	 * @param category|product $document Ascendant document
	 * @param string|null $filter Expression for filtering (AQL)
	 * @param string|null $sort Expression for sorting (AQL)
	 * @param int $page Страница
	 * @param int $amount Количество товаров на странице
	 * @param string|null $categories_merge Expression with paremeters to return for categories (AQL)
	 * @param string|null $products_merge Expression with paremeters to return for products (AQL)
	 * @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
	 * @param array &$errors Registry of errors
	 *
	 * @return array Массив с найденными вхождениями (может быть пустым)
	 */
	public static function search(
		category|product $document,
		?string $filter = 'v.deleted != true && v.hidden != true',
		?string $sort = 'v.position ASC, v.created DESC',
		int $page = 1,
		int $amount = 100,
		?string $categories_merge = null,
		?string $products_merge = null,
		array $parameters = [],
		array &$errors = []
	): array {
		try {
			if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
				if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
					// Initialized collections

					// Execute and exit (success)
					return is_array($result = collection::execute(
						sprintf(
							<<<'AQL'
							FOR v IN 1..1 INBOUND @document GRAPH @graph
								%s
								%s
								LIMIT @offset, @amount
								RETURN DISTINCT IS_SAME_COLLECTION(@category, v._id) ? MERGE(v, {_type: @category%s}) : MERGE(v, {_type: @product%s})
							AQL,
							empty($filter) ? '' : "FILTER $filter",
							empty($sort) ? '' : "SORT $sort",
							empty($categories_merge) ? '' : ", $categories_merge",
							empty($products_merge) ? '' : ", $products_merge"
						),
						[
							'category' => category::COLLECTION,
							'product' => product::COLLECTION,
							'graph' => 'catalog',
							'document' => $document->getId(),
							'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
							'amount' => $amount
						] + $parameters,
						errors: $errors
					)) ? $result : [$result];
				} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
			} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
		} catch (exception $e) {
			// Writing to the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return [];
	}

	/**	
	 * Banish the document from the catalog
	 *
	 * Removes all entry edges associated with the document
	 *
	 * @param categoru|product $document Document for banishing
	 * @param array &$errors Registry of errors
	 *
	 * @return void
	 */
	public static function banish(category|product $document,	array &$errors = []): void
	{
		try {
			if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
				if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
					// Initialized collections

					// Execute and exit (success)
					collection::execute(
						<<<'AQL'
							FOR d IN @@collection
								// FILTER d._from == @_id || d._to == @_id
								FILTER d._from == @_id
								REMOVE d IN @@collection
						AQL,
						[
							'@collection' => static::COLLECTION,
							'_id' => $document->getId()
						],
						errors: $errors
					);
				} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
			} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
		} catch (exception $e) {
			// Writing to the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return;
	}
}
