<?php

declare(strict_types=1);

namespace kodorvan\surikov\models;

// Files of the project
use kodorvan\surikov\models\core;

// Built-in libraries
use pdo,
exception;

/**
 * Account
 *
 * @package kodorvan\surikov\models
 *
 * @param int $identifier Identifier
 * @param string $password Password
 * @param string|null $hash Hash
 * @param int $time Time
 * @param array $permissions Permissions
 *
 * @method void __construct(array $parameters) Constructor
 * @method static static|null registrate(string $mail, string $password, bool $authenticate, array &$errors) Registrate
 * @method static static|null authenticate(string $mail, string $password, bool $remember, array &$errors) Authenticate
 * @method static bool deauthenticate(array &$errors) Deauthenticate
 * @method static static|null initialize(?int $identifier, array &$errors) Initialize
 * @method static array permissions(int $identifier, array &$errors) Permissions
 * @method static bool|null access(string $permission, array &$errors) Access
 * @method static static|null write(string $mail, string $password, array &$errors) Write
 * @method static static|null read(array $expressions, array &$errors) Read
 * @method static array|bool hash(int $identifier, string|null $hash, int|null $time, array &$errors) Hash
 *
 * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
 * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
 */
final class account extends core
{
	/**
	 * Identifier
	 *
	 * @var int $identifier
	 */
	public int $identifier;

	/**
	 * Mail
	 *
	 * @var string $mail
	 */
	public string $mail;

	/**
	 * Password
	 *
	 * @var string $password
	 */
	public string $password;

	/**
	 * Hash
	 *
	 * Hash of the session
	 *
	 * @var string|null $hash
	 */
	public ?string $hash = null;

	/**
	 * Time
	 *
	 * Time of the session
	 *
	 * @var int|null $time
	 */
	public ?int $time = null;

	/**
	 * Permissions
	 *
	 * @var array $permissions
	 */
	public array $permissions;

	/**
	 * Constructor
	 *
	 * @param array $parameters Parameters
	 *
	 * @return void
	 */
	public function __construct(array $parameters)
	{
		foreach ($parameters as $key => $value) {
			// Iterating over parameters
		
			// Writing the property
			if (property_exists($this, $key))
				$this->$key = $value;
		}
	}

	/**
	 * Registrate
	 *
	 * @param string $mail Mail
	 * @param string $password Password
	 * @param bool $authenticate Authenticate the account after registration?
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 */
	public static function registrate(string $mail, string $password, bool $authenticate = true, array &$errors = []): ?static
	{
		try {
			if (static::initialize(errors: $errors)) {
				// Authenticated the account

				// Exit (fail)
				throw new exception('Уже аутентифицирован');
			}

			if (empty($account = static::read(expressions: ['mail' => $mail]))) {
				// Not found the account

				if (static::write(mail: $mail, password: $password, errors: $errors)) {
					// Registrated the account

					if ($authenticate) {
						// Requested authentication after registration

						// Authentication
						$account = static::authenticate(mail: $mail, password: $password, remember: true, errors: $errors);

					}

					// Exit (success)
					return $account;
				}
			} else {
				// Found the account

				// Exit (success)
				return $account;
			}
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Authenticate
	 *
	 * @param string $mail Mail
	 * @param string $password Password
	 * @param bool $remember Increase time of the session?
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 */
	public static function authenticate(string $mail, string $password, bool $remember = false, array &$errors = []): ?static
	{
		try {
			if (static::initialize(errors: $errors)) {
				// Authenticated the account

				// Exit (fail)
				throw new exception('Уже аутентифицирован');
			}

			if (empty($account = static::read(expressions: ['mail' => $mail]))) {
				// Not found the account

				// Exit (fail)
				throw new exception('Не удалось найти аккаунт');
			}

			if (password_verify($password, $account->password)) {
				// Matches hashes

				// Initializing the session identifier
				session_id((string) $account->identifier);

				// Initializing the session name
				session_name('identifier');

				// Initializing the session
				session_start();

				// Calculating the session time
				$time = time() + ($remember ? 604800 : 86400);

				// Generating the session hash
				$hash = static::hash(identifier: (int) $account->identifier, hash: crypt($account->password, time() . $account->identifier), time: $time, errors: $errors)['hash'];

				// Initializing cookies
				setcookie(name: 'hash', value: $hash, expires_or_options: $time, path: '/', secure: true);

				// Exit (success)
				return $account;
			} else {
				// Not matches hashes

				// Exit (fail)
				throw new exception('Wrong password');
			}
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Deauthenticate
	 *
	 * @param array &$errors Registry of errors
	 *
	 * @return bool The session is deauthenticated?
	 */
	public static function deauthenticate(array &$errors = []): bool
	{
		try {
			if ($account = static::initialize(errors: $errors)) {
				// Authenticated the account

				// Initializing the request
				$request = static::$database->prepare("UPDATE `accounts` SET `hash` = null, `time` = 0 WHERE `identifier` = :identifier");

				// Initializing parameters of the request
				$parameters = [
					":identifier" => $account->identifier,
				];

				// Sending the request
				$request->execute($parameters);

				// Generating the answer
				$request->fetch(pdo::FETCH_ASSOC);

				// Deinitializing cookies
				setcookie('identifier', '', 0, path: '/', secure: true);
				setcookie('hash', '', 0, path: '/', secure: true);

				// Exit (fail)
				return true;
			} else {
				// Not authenticated the account

				// Exit (fail)
				throw new exception('Not authenticated');
			}
		} catch (exception $e) {
			// Exception

			// Writing into the registry of errors
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return false;
	}

	/**
	 * Initializing
	 *
	 * Initializing of the account
	 *
	 * @param int|null $identifier Identifier of the account
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 *
	 * @deprecated
	 */
	public static function init(?int $identifier = null, array &$errors = []): ?static
	{
		// Exit (success/fail)
		return static::initialize(identifier: $identifier, errors: $errors);
	}

	/**
	 * Initialize
	 *
	 * Initializing of the account
	 *
	 * @param int|null $identifier Identifier of the account
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 */
	public static function initialize(?int $identifier = null, array &$errors = []): ?static
	{
		try {
			if (isset($identifier)) {
				// Recaifer identifier of the account

				if (empty($account = static::read(['identifier' => $identifier]))) {
					// Found the account

					// Exit (fail)
					throw new exception('Не найден пользователь');
				}
			} else if (!empty($_COOKIE['identifier']) && !empty($_COOKIE['hash'])) {
				// Found cookie with the account data (expected that is already authenticated)

				if ($_COOKIE['hash'] === static::hash(identifier: (int) $_COOKIE['identifier'], errors: $errors)['hash']) {
					// Matches hashes from the cookie and the database
				} else {
					// Not matches hashes from the cookie and the database

					// Exit (fail)
					throw new exception('Вы аутентифицированы с другого устройства (не совпадают хеши аутентификации)');
				}

				if (
					empty($account = static::read([
						'identifier' => $_COOKIE['identifier'],
						'hash' => $_COOKIE['hash']
					]))
				) {
					// Not found the account or a connection with it

					// Exit (fail)
					throw new exception('Не найден пользователь или время аутентификации истекло');
				}
			} else {
				// Not found parameters for authentication

				// Exit (fail)
				return null;
			}

			// Initializing permissions
			$account->permissions = static::permissions(identifier: (int) $account->identifier, errors: $errors);

			// Exit (success)
			return $account;
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Permissions
	 *
	 * Read permissions of the account from the database
	 *
	 * @param int $identifier Identifier of the account
	 * @param array &$errors Registy of errors
	 *
	 * @return array Permissions of the account
	 */
	public static function permissions(int $identifier, array &$errors = []): array
	{
		try {
			// Initializing the request
			$request = static::$database->prepare("SELECT * FROM `permissions` WHERE `account` = :identifier");

			// Initializing parametes of the request
			$parameters = [
				":identifier" => $identifier
			];

			// Sending the request
			$request->execute($parameters);

			// Generating the answer
			if (empty($response = $request->fetch(pdo::FETCH_ASSOC))) {
				// Not found permissions

				// Exit (fail)
				throw new exception('Not found permissions');
			}

			// Deleting deprecated parameters
			unset($response['identifier']);

			// Exit (success)
			return $response;
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return [];
	}

	/**
	 * Access
	 *
	 * Check for the account authorizing by the permission
	 *
	 * @param string $permission Permission
	 * @param array &$errors Registry of errors
	 *
	 * @return bool|null Authorized?
	 */
	public function access(string $permission, array &$errors = []): ?bool
	{
		try {
			// Exit (success)
			return isset($this->permissions[$permission]) ? (bool) $this->permissions[$permission] : null;
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors['account'][] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Write
	 *
	 * Write the account into the database
	 *
	 * @param string $mail Mail
	 * @param string $password Password
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 */
	public static function write(string $mail, string $password, array &$errors = []): ?static
	{
		try {
			// Initializing parameters of the request
			$parameters = [];

			// Filtering the parameter
			if (filter_var($mail, FILTER_VALIDATE_EMAIL) === false)
				throw new exception('Не удалось распознать почту');
			if (iconv_strlen($mail) < 3)
				throw new exception('Длина почты должна быть не менее 3 символов');
			if (iconv_strlen($mail) > 60)
				throw new exception('Длина почты должна быть не более 80 символов');

			// Writing the parameter
			$parameters[':mail'] = $mail;

			// Filtering the parameter
			if (iconv_strlen($password) < 3)
				throw new exception('Длина пароля должна быть не менее 3 символов');
			if (iconv_strlen($password) > 60)
				throw new exception('Длина пароля должна быть не более 120 символов');

			// Writing the parameter
			$parameters[':password'] = password_hash($password, PASSWORD_BCRYPT);

			// Initializing the request
			$request = static::$database->prepare("INSERT INTO `accounts` (" . (isset($name) ? '`name`' : '') . (isset($name) && isset($mail) ? ', ' : '') . (isset($mail) ? '`mail`' : '') . ((isset($name) || isset($mail)) && isset($password) ? ', ' : '') . (isset($password) ? '`password`' : '') . ") VALUES (" . (isset($name) ? ':name' : '') . (isset($name) && isset($mail) ? ', ' : '') . (isset($mail) ? ':mail' : '') . ((isset($name) || isset($mail)) && isset($password) ? ', ' : '') . (isset($password) ? ':password' : '') . ")");

			// Sending the request
			$request->execute($parameters);

			// Generating the answer
			$request->fetch(pdo::FETCH_ASSOC);

			// Reading the account
			$account = static::read(expressions: ['mail' => $mail]);

			if ($account instanceof static) {
				// Registered the account

				// Initializing the request
				$request = static::$database->prepare("INSERT INTO `permissions` (`account`) VALUES (:identifier)");

				// Initializing parameters of the request
				$parameters = [
					':identifier' => $account->identifier
				];

				// Sending the request
				$request->execute($parameters);

				// Generating the answer
				$request->fetch(pdo::FETCH_ASSOC);

				// Exit (success)
				return $account;
			}
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Read
	 * 
	 * Read the account from the database
	 *
	 * @param array $expressions Request expressions [name => value]
	 * @param array &$errors Registry of errors
	 *
	 * @return static|null Account
	 */
	public static function read(array $expressions, array &$errors = []): ?static
	{
		try {
			// Initializing the request row
			$row = 'WHERE ';

			// Declaring the request parameters
			$parameters = [];

			foreach ($expressions as $name => $value) {
				// Iterating over request expressions

				// Generating the request row
				$row .= "`$name` = :$name &&";

				// Generating the request parameters
				$parameters[":$name"] = $value;
			}

			// Cleaning the request row
			$row = empty($expressions) ? '' : trim(trim($row, '&&'));

			// Initializing the request
			$request = static::$database->prepare("SELECT * FROM `accounts` $row LIMIT 1");

			// Sending the request
			$request->execute($parameters);

			// Generating the response
			$response = $request->fetch(pdo::FETCH_ASSOC);

			if (!empty($response) && $account = new static($response ?? [])) {
				// Found the account

				if ($permissions = static::permissions(identifier: (int) $account->identifier, errors: $errors)) {
					// Found the account permissions

					// Writing permissions into the account implementator
					$account->permissions = $permissions;
				}

				// Exit (success)
				return $account;
			} else {
				// Not found the account

				// Exit (fail)
				throw new exception('Not found the account');
			}
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors[] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return null;
	}

	/**
	 * Hash
	 *
	 * Write or read the account hash from the database
	 *
	 * @param int $identifier Identifier
	 * @param int|null $hash Hash
	 * @param string|null $time Time
	 * @param array &$errors Registry of errors
	 *
	 * @return array|bool Read: ['hash' => $hash, 'time' => $time]; Write: true; Error: false
	 */
	public static function hash(int $identifier, string|null $hash = null, int|null $time = null, array &$errors = []): array|bool
	{
		try {
			if (isset($hash, $time)) {
				// Write (received hash and time)

				// Initializing the request
				$request = static::$database->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `identifier` = :identifier");

				// Initializing the request parameters
				$parameters = [
					":identifier" => $identifier,
					":hash" => $hash,
					":time" => $time,
				];

				// Sending the request
				$request->execute($parameters);

				// Generating the answer
				$response = $request->fetch(pdo::FETCH_ASSOC);

				// Exit (success)
				return ['hash' => $hash, 'time' => $time];
			} else {
				// Read (not received hash and time)

				// Initializing the request
				$request = static::$database->prepare("SELECT `hash`, `time` FROM `accounts` WHERE `identifier` = :identifier");

				// Initializing the request parameters
				$parameters = [
					":identifier" => $identifier
				];

				// Sending the request
				$request->execute($parameters);

				// Generating the answer
				extract((array) $request->fetch(pdo::FETCH_ASSOC));

				if (!empty($response['time']) && $response['time'] <= time()) {
					// Expired the hash

					// Initializing the request
					$request = static::$database->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `identifier` = :identifier");

					// Initializing the request parameters
					$parameters = [
						":identifier" => $identifier,
						":hash" => null,
						":time" => null,
					];

					// Sending the request
					$request->execute($parameters);

					// Generating the answer
					$response = $request->fetch(pdo::FETCH_ASSOC);

					// Exit (fail)
					throw new exception('Expired the hash');
				}

				// Exit (success)
				return ['hash' => $hash, 'time' => $time];
			}
		} catch (exception $e) {
			// Exception

			// Writing into the errors registry
			$errors['account'][] = [
				'text' => $e->getMessage(),
				'file' => $e->getFile(),
				'line' => $e->getLine(),
				'stack' => $e->getTrace()
			];
		}

		// Exit (fail)
		return false;
	}
}
