<?php

declare(strict_types=1);

namespace mirzaev\ebala\models;

// Файлы проекта
use mirzaev\ebala\models\account,
	mirzaev\ebala\models\traits\status;

// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
	mirzaev\arangodb\document;

// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;

// Встроенные библиотеки
use exception;

/**
 * Модель сессий
 *
 * @package mirzaev\ebala\models
 * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
 */
final class session extends core
{
	use status;

	/**
	 * Collection name in ArangoDB
	 */
	final public const COLLECTION = 'session';

	/**
	 * Инстанция документа в базе данных 
	 */
	protected readonly _document $document;

	/**
	 * Конструктор 
	 *
	 * Инициализация сессии и запись в свойство $this->document
	 *
	 * @param ?string $hash Хеш сессии в базе данных
	 * @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
	 * @param array &$errors Реестр ошибок
	 *
	 * @return static Инстанция сессии
	 */
	public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
	{
		try {
			if (collection::init(static::$arangodb->session, self::COLLECTION)) {
				// Инициализирована коллекция

				if (
					isset($hash)
					&& $session = collection::search($this::$arangodb->session, sprintf(
						<<<AQL
						FOR d IN %s
							FILTER d.hash == '%s' && d.expires > %d && d.active == true
							RETURN d
					AQL,
						self::COLLECTION,
						$hash,
						time()
					))
				) {
					// Received session hash and session found

					// Запись в свойство
					$this->document = $session;
				} else {
					// Не найдена сессия

					// Запись сессии в базу данных
					$_id = document::write($this::$arangodb->session, self::COLLECTION, [
						'active' => true,
						'expires' => $expires ?? time() + 604800,
						'ip' => $_SERVER['REMOTE_ADDR'],
						'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
						'referer' => $_SERVER['HTTP_REFERER'] ?? null,
						'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? null
					]);

					if ($session = collection::search($this::$arangodb->session, sprintf(
						<<<AQL
							FOR d IN %s
								FILTER d._id == '$_id' && d.expires > %d && d.active == true
								RETURN d
						AQL,
						self::COLLECTION,
						time()
					))) {
						// Найдена только что созданная сессия

						// Запись хеша
						$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));

						if (document::update($this::$arangodb->session, $session)) {
							// Записано обновление

							// Запись в свойство
							$this->document = $session;
						} else throw new exception('Не удалось записать данные сессии');
					} else throw new exception('Не удалось создать или найти созданную сессию');
				}
			} else throw new exception('Не удалось инициализировать коллекцию');
		} catch (exception $e) {
			// Запись в реестр ошибок
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}
	}

	public function search(string $hash, array &$errors = []): bool
	{
		try {
			search_arangodb:

			// Search the session data in ArangoDB
			$_document = collection::search(static::$arangodb->session, sprintf(
				<<<AQL
						FOR d IN %s
							FILTER d.hash == '$hash' && d.expires > %d && d.active == true
							RETURN d
					AQL,
				self::COLLECTION,
				time()
			));

			if ($_document instanceof _document) {
				// The instance found

				// Write the session data to the property
				$this->document = $_document;

				// Exit (success)
				return true;
			}
		} catch (exception $e) {
			// Write to the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return false;
	}


	public function __destruct()
	{
		// Закрыть сессию
	}

	/**
	 * Инициализировать связь сессии с аккаунтом
	 *
	 * Ищет связь сессии с аккаунтом, если не находит, то создаёт её
	 *
	 * @param account $account Инстанция аккаунта
	 * @param array &$errors Реестр ошибок
	 *
	 * @return bool Связана сессия с аккаунтом?
	 */
	public static function connect(string $session, string $account, array &$errors = []): bool
	{
		try {
			if (
				collection::init(static::$arangodb->session, self::COLLECTION)
				&& collection::init(static::$arangodb->session, account::COLLECTION)
				&& collection::init(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
			) {
				// Инициализированы коллекции

				if (
					collection::search(static::$arangodb->session, sprintf(
						<<<AQL
							FOR document IN %s
								FILTER document._from == '%s' && document._to == '%s'
								LIMIT 1
								RETURN document
						AQL,
						self::COLLECTION . '_edge_' . account::COLLECTION,
						$session,
						$account
					)) instanceof _document
					|| document::write(static::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
						'_from' => $session,
						'_to' => $account
					])
				) {
					// Найдено, либо создано ребро: session -> account

					return true;
				} else throw new exception('Не удалось создать ребро: session -> account');
			} else throw new exception('Не удалось инициализировать коллекции');
		} catch (exception $e) {
			// Запись в реестр ошибок
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		return false;
	}

	/**
	 * Найти связанный аккаунт
	 *
	 * @param array &$errors Реестр ошибок
	 *
	 * @return ?account Инстанция аккаунта, если удалось найти
	 */
	public function account(array &$errors = []): ?account
	{
		try {
			if (
				collection::init($this::$arangodb->session, self::COLLECTION)
				&& collection::init($this::$arangodb->session, account::COLLECTION)
				&& collection::init($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
			) {
				// Инициализированы коллекции

				// Инициализация инстанции аккаунта
				$account = new account;

				// Поиск инстанции аккаунта в базе данных
				$instance = $account->instance(collection::search($this::$arangodb->session, sprintf(
					<<<AQL
						FOR d IN %s
							LET e = (
								FOR e IN %s
								FILTER e._from == '%s'
								SORT e._key DESC
								LIMIT 1
								RETURN e
							)
							FILTER d._id == e[0]._to
							SORT d.created DESC
							LIMIT 1
							RETURN d
					AQL,
					account::COLLECTION,
					self::COLLECTION . '_edge_' . account::COLLECTION,
					$this->getId()
				)));

				// Возврат (успех)
				return $instance instanceof _document ? $account : throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
			} else throw new exception('Не удалось инициализировать коллекцию');
		} catch (exception $e) {
			// Запись в реестр ошибок
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		return null;
	}

	/**
	 * Записать в буфер сессии
	 *
	 * @param array $data Данные для записи
	 * @param array &$errors Реестр ошибок
	 *
	 * @return bool Записаны данные в буфер сессии?
	 */
	public function write(array $data, array &$errors = []): bool
	{
		try {
			if (collection::init($this::$arangodb->session, self::COLLECTION)) {
				// Инициализирована коллекция

				// Проверка инициализированности инстанции документа из базы данных
				if (!isset($this->document)) throw new exception('Не инициализирована инстанция документа из базы данных');

				// Запись параметров в инстанцию документа из базы данных
				$this->document->buffer = array_replace_recursive(
					$this->document->buffer ?? [],
					[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
				);

				// Запись в базу данных и возврат (успех)
				return document::update($this::$arangodb->session, $this->document) ? true : throw new exception('Не удалось записать данные в буфер сессии');
			} else throw new exception('Не удалось инициализировать коллекцию');
		} catch (exception $e) {
			// Запись в реестр ошибок
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		return false;
	}

	/**
	 * Записать
	 *
	 * Записывает свойство в инстанцию документа сессии из базы данных
	 *
	 * @param string $name Название
	 * @param mixed $value Содержимое
	 *
	 * @return void
	 */
	public function __set(string $name, mixed $value = null): void
	{
		$this->document->{$name} = $value;
	}

	/**
	 * Прочитать
	 *
	 * Читает свойство из инстанции документа сессии из базы данных
	 *
	 * @param string $name Название
	 *
	 * @return mixed Данные свойства инстанции сессии или инстанции документа сессии из базы данных
	 */
	public function __get(string $name): mixed
	{
		return match ($name) {
			'arangodb' => $this::$arangodb,
			default => $this->document->{$name}
		};
	}

	/**
	 * Проверить инициализированность
	 *
	 * Проверяет инициализированность свойства в инстанции документа сессии из базы данных
	 *
	 * @param string $name Название
	 *
	 * @return bool Свойство инициализировано?
	 */
	public function __isset(string $name): bool
	{
		return isset($this->document->{$name});
	}

	/**
	 * Удалить
	 *
	 * Деинициализировать свойство в инстанции документа сессии из базы данных
	 *
	 * @param string $name Название
	 *
	 * @return void
	 */
	public function __unset(string $name): void
	{
		unset($this->document->{$name});
	}

	/**
	 * Выполнить метод
	 *
	 * Выполнить метод в инстанции документа сессии из базы данных
	 *
	 * @param string $name Название
	 * @param array $arguments Аргументы
	 */
	public function __call(string $name, array $arguments = [])
	{
		if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
	}
}
