<?php

declare(strict_types=1);

namespace mirzaev\ebala\models;

// Project files
use mirzaev\ebala\models\traits\instance,
	mirzaev\ebala\models\traits\status;

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

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

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

/**
 * Модель аккаунта
 *
 * @package mirzaev\ebala\models
 * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
 */
final class account extends core
{
	use instance, status;

	/**
	 * Коллекция
	 */
	final public const COLLECTION = 'account';

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

	/**
	 * Конструктор
	 *
	 * @param ?session $session Инстанция сессии
	 * @param ?string $authenticate Аутентифицировать аккаунт? Если да, то какой категории? ([worker|market|operator|administrator] из $_SERVER['INTERFACE'])
	 * @param array &$errors Реестр ошибок
	 *
	 * @return static Инстанция аккаунта
	 */
	public function __construct(?session $session = null, ?string $authenticate = null, array &$errors = [])
	{
		try {
			if (isset($session)) {
				// Получена инстанция сессии

				if ($account = $session->account()) {
					// Найден связанный с сессией аккаунт

					// Инициализация инстанции документа аккаунта в базе данных
					$this->document = $account->document;

					// Связь сессии с аккаунтом
					session::connect($session->getId(), $this->document->getId(), $errors);

					// Блокировка доступа
					if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
					else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
					else if ($account->type === 'worker')
						if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
						else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');

					return $this;
				} else {
					// Не найден связанный с сессией аккаунт
					if (
						match ($authenticate) {
							'worker', 'market', 'operator', 'administrator' => true,
							default => false
						}
					) {
						// Запрошена аутентификация

						if (!empty($session->buffer['worker']['entry']['number'])) {
							// Найден номер сотрудника в буфере сессии

							if (!empty($session->buffer['worker']['entry']['password'])) {
								// Найден пароль в буфере сессии

								if (($account = self::read('d.number == "' . $session->buffer['worker']['entry']['number'] . '" && d.type == "worker"', amount: 1, errors: $errors)) instanceof _document) {
									// Найден аккаунт сотрудника (игнорируются ошибки)

									if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['worker']['entry']['password'])) {
										// Аутентифицирован аккаунт (прошёл проверку пароль)

										// Инициализация инстанции документа аккаунта в базе данных
										$this->document = $account;

										// Связь сессии с аккаунтом
										session::connect($session->getId(), $this->document->getId(), $errors);

										// Удаление использованных данных из буфера сессии
										$session->write(['entry' => ['number' => null, 'password' => null]]);

										// Блокировка доступа
										if ($account?->active !== true) throw new exception('Свяжитесь с оператором');
										else if ($account?->banned === true) throw new exception('Свяжитесь с оператором');
										else if ($account->type === 'worker')
											if (($worker = account::worker($account->getId()))?->active !== true) throw new exception('Свяжитесь с оператором');
											else if ($worker?->fired === true) throw new exception('Свяжитесь с оператором');

										// Выход (успех)
										return $this;
									} else throw new exception('Неправильный пароль');
								} else throw new exception('Не найден аккаунт');
							} else throw new exception('Не найден пароль в буфере сессии');
						} else if (!empty($session->buffer['operator']['entry']['_key'])) {
							// Найден идентификатор оператора в буфере сессии

							if (!empty($session->buffer['operator']['entry']['password'])) {
								// Найден пароль в буфере сессии

								if (($account = self::read('d._key == "' . $session->buffer['operator']['entry']['_key'] . '" && d.type == "operator"', amount: 1)) instanceof _document) {
									// Найден аккаунт оператора (игнорируются ошибки)

									if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['operator']['entry']['password'])) {
										// Аутентифицирован аккаунт (прошёл проверку пароль)

										// Инициализация инстанции документа аккаунта в базе данных
										$this->document = $account;

										// Связь сессии с аккаунтом
										session::connect($session->getId(), $this->document->getId(), $errors);

										// Удаление использованных данных из буфера сессии
										$session->write(['entry' => ['_key' => null, 'password' => null]]);

										// Выход (успех)
										return $this;
									} else throw new exception('Неправильный пароль');
								}
							} else throw new exception('Не найден пароль в буфере сессии');
						} else if (!empty($session->buffer['market']['entry']['id'])) {
							// Найден идентификатор магазина в буфере сессии

							if (!empty($session->buffer['market']['entry']['password'])) {
								// Найден пароль в буфере сессии

								if (($account = market::account(market::read('d.id == "' . $session->buffer['market']['entry']['id'] . '"', amount: 1)?->getId()) ?? null) instanceof _document) {
									// Найден аккаунт (игнорируются ошибки)

									if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['market']['entry']['password'])) {
										// Аутентифицирован аккаунт (прошёл проверку пароль)

										// Инициализация инстанции документа аккаунта в базе данных
										$this->document = $account;

										// Связь сессии с аккаунтом
										session::connect($session->getId(), $this->document->getId(), $errors);

										// Удаление использованных данных из буфера сессии
										$session->write(['entry' => ['_key' => null, 'password' => null]]);

										// Выход (успех)
										return $this;
									} else throw new exception('Неправильный пароль');
								}
							} else throw new exception('Не найден пароль в буфере сессии');
						} else if (!empty($session->buffer['administrator']['entry'])) {
							// Найден идентификатор администратора в буфере сессии

							if (!empty($session->buffer['administrator']['entry']['password'])) {
								// Найден пароль в буфере сессии

								if (($account = self::read('d._key == "' . $session->buffer['administrator']['entry']['_key'] . '" && d.type == "administrator"', amount: 1)) instanceof _document) {
									// Найден аккаунт администратора (игнорируются ошибки)

									if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['administrator']['entry']['password'])) {
										// Аутентифицирован аккаунт (прошёл проверку пароль)

										// Инициализация инстанции документа аккаунта в базе данных
										$this->document = $account;

										// Связь сессии с аккаунтом
										session::connect($session->getId(), $this->document->getId(), $errors);

										// Удаление использованных данных из буфера сессии
										$session->write(['entry' => ['_key' => null, 'password' => null]]);

										// Выход (успех)
										return $this;
									} else throw new exception('Неправильный пароль');
								}
							} 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()
			];
		}
	}

	/**
	 * Найти связанного с аккаунтом сотрудника
	 *
	 * @param string $_id Идентификатор аккаунта связанного с сотрудником
	 * @param array &$errors Реестр ошибок
	 *
	 * @return ?_document Инстанция документа сотрудника в базе данных, если найдена
	 */
	public static function worker(string $_id, array &$errors = []): ?_document
	{
		try {
			if (collection::init(static::$arangodb->session, worker::COLLECTION)) {
				// Инициализирована коллекция

				// Поиск сотрудника
				$worker = collection::search(
					static::$arangodb->session,
					sprintf(
						<<<'AQL'
							FOR d IN %s
								LET e = (
									FOR e IN %s
										FILTER e._from == '%s'
										SORT e.created DESC, e._key DESC
										LIMIT 1
										RETURN e
								)
								FILTER d._id == e[0]._to
								SORT d.created DESC, d._key DESC
								LIMIT 1
								RETURN d
						AQL,
						worker::COLLECTION,
						static::COLLECTION . '_edge_worker',
						$_id
					)
				);

				// Возврат (успех)
				return $worker instanceof _document ? $worker : 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 string $_id Идентификатор аккаунта связанного с магазином
	 * @param array &$errors Реестр ошибок
	 *
	 * @return ?_document Инстанция документа магазина в базе данных, если найдена
	 */
	public static function market(string $_id, array &$errors = []): ?_document
	{
		try {
			if (
				collection::init(static::$arangodb->session, static::COLLECTION)
				&& collection::init(static::$arangodb->session, market::COLLECTION)
				&& collection::init(static::$arangodb->session, static::COLLECTION . '_edge_market', true)
			) {
				// Инициализированы коллекции

				// Поиск магазина
				$market = collection::search(
					static::$arangodb->session,
					sprintf(
						<<<'AQL'
							FOR d IN %s
								LET e = (
									FOR e IN %s
										FILTER e._from == '%s'
										SORT e.created DESC, e._key DESC
										LIMIT 1
										RETURN e
								)
								FILTER d._id == e[0]._to
								SORT d.created DESC, d.id DESC
								LIMIT 1
								RETURN d
						AQL,
						market::COLLECTION,
						static::COLLECTION . '_edge_market',
						$_id
					)
				);

				return $market instanceof _document ? $market : 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 string $account Идентификатор инстанции документа аккаунта в базе данных
	 * @param string $target Идентификатор инстанции документа цели в базе данны (подразумевается сотрудник или магазин)
	 * @param string $type Тип подключения (worker|market)
	 * @param array &$errors Реестр ошибок
	 *
	 * @return bool Связан аккаунт с сотрудником?
	 */
	public static function connect(string $account, string $target, string $type = 'worker', array &$errors = []): bool
	{
		try {
			if (
				collection::init(static::$arangodb->session, $type)
				&& collection::init(static::$arangodb->session, self::COLLECTION)
				&& collection::init(static::$arangodb->session, self::COLLECTION . "_edge_$type", true)
			) {
				// Инициализированы коллекции

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

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

		return false;
	}

	/**
	 * Создать
	 *
	 * @param array $data Данные аккаунта
	 * @param array &$errors Реестр ошибок
	 *
	 * @return string|null Идентификатор документа, если он был создан
	 */
	public static function create(array $data = [], array &$errors = []): ?string
	{
		try {
			if (collection::init(static::$arangodb->session, self::COLLECTION))
				if ($id = (string) document::write(static::$arangodb->session, self::COLLECTION, $data + ['active' => true])) return $id;
				else 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 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 $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);
	}
}
