<?php
declare(strict_types=1);

/**
 * Controller to handle user actions.
 */
class FreshRSS_user_Controller extends FreshRSS_ActionController {
	/**
	 * The username is also used as folder name, file name, and part of SQL table name.
	 * '_' is a reserved internal username.
	 */
	public const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@\-]{1,38}|[0-9a-zA-Z])';

	public static function checkUsername(string $username): bool {
		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
	}

	/**
	 * Validate an email address, supports internationalized addresses.
	 *
	 * @param string $email The address to validate
	 * @return bool true if email is valid, else false
	 */
	private static function validateEmailAddress(string $email): bool {
		$mailer = new PHPMailer\PHPMailer\PHPMailer();
		$mailer->CharSet = 'utf-8';
		$punyemail = $mailer->punyencodeAddress($email);
		return PHPMailer\PHPMailer\PHPMailer::validateAddress($punyemail, 'html5');
	}

	/**
	 * @return list<string>
	 */
	public static function listUsers(): array {
		$final_list = [];
		$base_path = join_path(DATA_PATH, 'users');
		$dir_list = array_values(array_diff(
			scandir($base_path) ?: [],
			['..', '.', Minz_User::INTERNAL_USER]
		));
		foreach ($dir_list as $file) {
			if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) {
				$final_list[] = $file;
			}
		}
		return $final_list;
	}

	public static function userExists(string $username): bool {
		$config_path = USERS_PATH . '/' . $username . '/config.php';
		if (@file_exists($config_path)) {
			return true;
		} elseif (@file_exists($config_path . '.bak.php')) {
			Minz_Log::warning('Config for user “' . $username . '” not found. Attempting to restore from backup.', ADMIN_LOG);
			if (!copy($config_path . '.bak.php', $config_path)) {
				@unlink($config_path);
				return false;
			}
			return @file_exists($config_path);
		}
		return false;
	}

	/**
	 * Return if the maximum number of registrations has been reached.
	 * Note a max_registrations of 0 means there is no limit.
	 *
	 * @return bool true if number of users >= max registrations, false otherwise.
	 */
	public static function max_registrations_reached(): bool {
		$limit_registrations = FreshRSS_Context::systemConf()->limits['max_registrations'];
		$number_accounts = count(self::listUsers());
		return $limit_registrations > 0 && $number_accounts >= $limit_registrations;
	}

	/** @param array<string,mixed> $userConfigUpdated */
	public static function updateUser(string $user, ?string $email, string $passwordPlain, array $userConfigUpdated = []): bool {
		$userConfig = FreshRSS_UserConfiguration::getForUser($user);
		if ($userConfig === null) {
			return false;
		}

		if ($email !== null && $userConfig->mail_login !== $email) {
			$userConfig->mail_login = $email;

			if (FreshRSS_Context::systemConf()->force_email_validation) {
				$userConfig->email_validation_token = hash('sha256', FreshRSS_Context::systemConf()->salt . $email . random_bytes(32));
				$mailer = new FreshRSS_User_Mailer();
				$mailer->send_email_need_validation($user, $userConfig);
			}
		}

		if ($passwordPlain != '') {
			$passwordHash = FreshRSS_password_Util::hash($passwordPlain);
			$userConfig->passwordHash = $passwordHash;
			if ($user === Minz_User::name()) {
				FreshRSS_Context::userConf()->passwordHash = $passwordHash;
			}
		}

		foreach ($userConfigUpdated as $configName => $configValue) {
			if ($configName !== '' && $configValue !== null) {
				$userConfig->_attribute($configName, $configValue);
			}
		}

		$ok = $userConfig->save();
		return $ok;
	}

	public function updateAction(): void {
		if (!FreshRSS_Auth::hasAccess('admin')) {
			Minz_Error::error(403);
		}

		if (Minz_Request::isPost()) {
			if (self::reauthRedirect()) {
				return;
			}

			$username = Minz_Request::paramString('username');
			$newPasswordPlain = Minz_User::name() !== $username ? Minz_Request::paramString('newPasswordPlain', true) : '';

			$ok = self::updateUser($username, null, $newPasswordPlain, [
				'token' => Minz_Request::paramString('token') ?: null,
			]);

			if ($ok) {
				$isSelfUpdate = Minz_User::name() === $username;
				if ($newPasswordPlain == '' || !$isSelfUpdate) {
					Minz_Request::good(
						_t('feedback.user.updated', $username),
						['c' => 'user', 'a' => 'manage'],
						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
					);
				} else {
					Minz_Request::good(
						_t('feedback.profile.updated'),
						['c' => 'index', 'a' => 'index'],
						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
					);
				}
			} else {
				Minz_Request::bad(_t('feedback.user.updated.error', $username), ['c' => 'user', 'a' => 'manage']);
			}
		}
	}

	/**
	 * This action displays the user profile page.
	 */
	public function profileAction(): void {
		if (!FreshRSS_Auth::hasAccess()) {
			Minz_Error::error(403);
		}

		$email_not_verified = FreshRSS_Context::userConf()->email_validation_token != '';
		$this->view->disable_aside = false;
		if ($email_not_verified) {
			$this->view->disable_aside = true;
		}

		FreshRSS_View::prependTitle(_t('conf.profile.title') . ' · ');

		FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/bcrypt.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/bcrypt.js')));

		if (Minz_Request::isPost() && Minz_User::name() != null) {
			$old_email = FreshRSS_Context::userConf()->mail_login;

			$email = Minz_Request::paramString('email');

			$challenge = Minz_Request::paramString('challenge');
			$newPasswordPlain = '';
			if ($challenge !== '') {
				$username = Minz_User::name();
				$nonce = Minz_Session::paramString('nonce');

				$newPasswordPlain = Minz_Request::paramString('newPasswordPlain', plaintext: true);
				$confirmPasswordPlain = Minz_Request::paramString('confirmPasswordPlain', plaintext: true);

				if (!FreshRSS_FormAuth::checkCredentials(
					$username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge
					) || strlen($newPasswordPlain) < 7) {
					Minz_Session::_param('open', true); // Auto-expand `change password` section
					Minz_Request::bad(
						_t('feedback.auth.login.invalid'),
						['c' => 'user', 'a' => 'profile']
					);
					return;
				}

				if ($newPasswordPlain !== $confirmPasswordPlain) {
					Minz_Session::_param('open', true); // Auto-expand `change password` section
					Minz_Request::bad(
						_t('feedback.profile.passwords_dont_match'),
						['c' => 'user', 'a' => 'profile']
					);
					return;
				}

				Minz_Session::regenerateID('FreshRSS');
			}

			if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
				Minz_Request::bad(
					_t('user.email.feedback.required'),
					['c' => 'user', 'a' => 'profile']
				);
			}

			if (!empty($email) && !self::validateEmailAddress($email)) {
				Minz_Request::bad(
					_t('user.email.feedback.invalid'),
					['c' => 'user', 'a' => 'profile']
				);
			}

			$ok = self::updateUser(
				Minz_User::name(),
				$email,
				$newPasswordPlain,
				[
					'token' => Minz_Request::paramString('token'),
				]
			);

			Minz_Session::_param('passwordHash', FreshRSS_Context::userConf()->passwordHash);

			if ($ok) {
				if (FreshRSS_Context::systemConf()->force_email_validation && $email !== $old_email) {
					Minz_Request::good(
						_t('feedback.profile.updated'),
						['c' => 'user', 'a' => 'validateEmail'],
						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
					);
				} else {
					Minz_Request::good(
						_t('feedback.profile.updated'),
						['c' => 'user', 'a' => 'profile'],
						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
					);
				}
			} else {
				Minz_Request::bad(_t('feedback.profile.error'), ['c' => 'user', 'a' => 'profile']);
			}
		}
	}

	public static function reauthRedirect(): bool {
		$url_redirect = [
			'c' => 'user',
			'a' => 'manage',
			'params' => [],
		];
		$username = Minz_Request::paramStringNull('username');
		if ($username !== null) {
			$url_redirect['a'] = 'details';
			$url_redirect['params']['username'] = $username;
		}
		return FreshRSS_Auth::requestReauth($url_redirect);
	}

	public function purgeAction(): void {
		if (!FreshRSS_Auth::hasAccess('admin')) {
			Minz_Error::error(403);
		}

		if (!Minz_Request::isPost()) {
			Minz_Error::error(403);
		}

		if (self::reauthRedirect()) {
			return;
		}

		$username = Minz_Request::paramString('username');

		if (!FreshRSS_UserDAO::exists($username)) {
			Minz_Error::error(404);
		}

		$feedDAO = FreshRSS_Factory::createFeedDao($username);
		$feedDAO->purge();
	}

	/**
	 * This action displays the user management page.
	 */
	public function manageAction(): void {
		if (!FreshRSS_Auth::hasAccess('admin')) {
			Minz_Error::error(403);
		}

		if (self::reauthRedirect()) {
			return;
		}

		FreshRSS_View::prependTitle(_t('admin.user.title') . ' · ');

		if (Minz_Request::isPost()) {
			$action = Minz_Request::paramString('action');
			switch ($action) {
				case 'delete':
					$this->deleteAction();
					break;
				case 'update':
					$this->updateAction();
					break;
				case 'purge':
					$this->purgeAction();
					break;
				case 'promote':
					$this->promoteAction();
					break;
				case 'demote':
					$this->demoteAction();
					break;
				case 'enable':
					$this->enableAction();
					break;
				case 'disable':
					$this->disableAction();
					break;
			}
		}

		$this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation;
		$this->view->current_user = Minz_Request::paramString('u');

		$fast = false;
		$startTime = time();
		foreach (self::listUsers() as $user) {
			if (!$fast && (time() - $startTime >= 3)) {
				// Disable detailed user statistics if it takes too long, and will retrieve them asynchronously via JavaScript
				$fast = true;
			}
			$this->view->users[$user] = $this->retrieveUserDetails($user, $fast);
		}
	}

	/**
	 * @param array<string,mixed> $userConfigOverride
	 * @throws Minz_ConfigurationNamespaceException
	 * @throws Minz_PDOConnectionException
	 */
	public static function createUser(string $new_user_name, ?string $email, string $passwordPlain,
		array $userConfigOverride = [], bool $insertDefaultFeeds = true): bool {
		$userConfig = [];

		$customUserConfigPath = join_path(DATA_PATH, 'config-user.custom.php');
		if (file_exists($customUserConfigPath)) {
			$customUserConfig = include $customUserConfigPath;
			if (is_array($customUserConfig)) {
				$userConfig = $customUserConfig;
			}
		}

		$userConfig = array_merge($userConfig, $userConfigOverride);

		$ok = self::checkUsername($new_user_name);
		$homeDir = join_path(DATA_PATH, 'users', $new_user_name);
		// create basepath if missing
		if (!is_dir(join_path(DATA_PATH, 'users'))) {
			$ok &= mkdir(join_path(DATA_PATH, 'users'), 0770, true);
		}
		$configPath = '';

		if ($ok) {
			if (!Minz_Translate::exists(is_string($userConfig['language'] ?? null) ? $userConfig['language'] : '')) {
				$userConfig['language'] = Minz_Translate::DEFAULT_LANGUAGE;
			}

			$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', self::listUsers()), true);	//Not an existing user, case-insensitive

			$configPath = join_path($homeDir, 'config.php');
			$ok &= !file_exists($configPath);
		}
		if ($ok) {
			// $homeDir must not exist beforehand,
			// otherwise it might be multiple remote parties racing to register one username
			$ok = mkdir($homeDir, 0770, true);
			if ($ok) {
				$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
			}
		}
		if ($ok) {
			$newUserDAO = FreshRSS_Factory::createUserDao($new_user_name);
			$ok &= $newUserDAO->createUser();

			if ($ok && $insertDefaultFeeds) {
				$opmlPath = DATA_PATH . '/opml.xml';
				if (!file_exists($opmlPath)) {
					$opmlPath = FRESHRSS_PATH . '/opml.default.xml';
				}
				$importController = new FreshRSS_importExport_Controller();
				try {
					$importController->importFile($opmlPath, $opmlPath, $new_user_name);
				} catch (Exception $e) {
					Minz_Log::error('Error while importing default OPML for user ' . $new_user_name . ': ' . $e->getMessage());
				}
			}

			$ok &= self::updateUser($new_user_name, $email, $passwordPlain);
		}
		return (bool)$ok;
	}

	/**
	 * This action creates a new user.
	 *
	 * Request parameters are:
	 *   - new_user_language
	 *   - new_user_name
	 *   - new_user_email
	 *   - new_user_passwordPlain
	 *   - r (i.e. a redirection url, optional)
	 *
	 * @todo clean up this method. Idea: write a method to init a user with basic information.
	 */
	public function createAction(): void {
		if (!FreshRSS_Auth::hasAccess('admin') && self::max_registrations_reached()) {
			Minz_Error::error(403);
		}

		if (FreshRSS_Auth::hasAccess('admin') && self::reauthRedirect()) {
			return;
		}

		if (Minz_Request::isPost()) {
			$new_user_name = Minz_Request::paramString('new_user_name');
			$email = Minz_Request::paramString('new_user_email');
			$passwordPlain = Minz_Request::paramString('new_user_passwordPlain', true);
			$badRedirectUrl = [
				'c' => Minz_Request::paramString('originController') ?: 'auth',
				'a' => Minz_Request::paramString('originAction') ?: 'register',
			];

			if (!self::checkUsername($new_user_name)) {
				Minz_Request::bad(
					_t('user.username.invalid'),
					$badRedirectUrl
				);
			}

			if (FreshRSS_UserDAO::exists($new_user_name)) {
				Minz_Request::bad(
					_t('user.username.taken', $new_user_name),
					$badRedirectUrl
				);
			}

			if (!FreshRSS_password_Util::check($passwordPlain)) {
				Minz_Request::bad(
					_t('user.password.invalid'),
					$badRedirectUrl
				);
			}

			if (!FreshRSS_Auth::hasAccess('admin')) {
				// TODO: We may want to ask the user to accept TOS before first login
				$tos_enabled = file_exists(TOS_FILENAME);
				$accept_tos = Minz_Request::paramBoolean('accept_tos');
				if ($tos_enabled && !$accept_tos) {
					Minz_Request::bad(_t('user.tos.feedback.invalid'), $badRedirectUrl);
				}
			}

			if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
				Minz_Request::bad(
					_t('user.email.feedback.required'),
					$badRedirectUrl
				);
			}

			if (!empty($email) && !self::validateEmailAddress($email)) {
				Minz_Request::bad(
					_t('user.email.feedback.invalid'),
					$badRedirectUrl
				);
			}

			$is_admin = false;
			if (FreshRSS_Auth::hasAccess('admin')) {
				$is_admin = Minz_Request::paramBoolean('new_user_is_admin');
			}

			$ok = self::createUser($new_user_name, $email, $passwordPlain, [
				'language' => Minz_Request::paramString('new_user_language') ?: FreshRSS_Context::userConf()->language,
				'timezone' => Minz_Request::paramString('new_user_timezone'),
				'is_admin' => $is_admin,
				'enabled' => true,
			]);
			Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
			$_POST['new_user_passwordPlain'] = '';
			invalidateHttpCache();

			// If the user has admin access, it means he’s already logged in
			// and we don’t want to login with the new account. Otherwise, the
			// user just created its account himself so he probably wants to
			// get started immediately.
			if ($ok && !FreshRSS_Auth::hasAccess('admin')) {
				$user_conf = FreshRSS_UserConfiguration::getForUser($new_user_name);
				if ($user_conf !== null) {
					Minz_Session::_params([
						Minz_User::CURRENT_USER => $new_user_name,
						'passwordHash' => $user_conf->passwordHash,
						'csrf' => false,
					]);
					FreshRSS_Auth::giveAccess();
				} else {
					$ok = false;
				}
			}

			if ($ok) {
				Minz_Request::setGoodNotification(_t('feedback.user.created', $new_user_name));
			} else {
				Minz_Request::setBadNotification(_t('feedback.user.created.error', $new_user_name));
			}
		}

		if (FreshRSS_Auth::hasAccess('admin')) {
			$redirect_url = ['c' => 'user', 'a' => 'manage'];
		} else {
			$redirect_url = ['c' => 'index', 'a' => 'index'];
		}
		Minz_Request::forward($redirect_url, true);
	}

	public static function deleteUser(string $username): bool {
		$ok = self::checkUsername($username);
		if ($ok) {
			$default_user = FreshRSS_Context::systemConf()->default_user;
			$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
		}
		$user_data = join_path(DATA_PATH, 'users', $username);
		$ok &= is_dir($user_data);
		if ($ok) {
			FreshRSS_fever_Util::deleteKey($username);
			Minz_ModelPdo::$usesSharedPdo = false;
			$oldUserDAO = FreshRSS_Factory::createUserDao($username);
			$ok &= $oldUserDAO->deleteUser();
			Minz_ModelPdo::$usesSharedPdo = true;
			$ok &= recursive_unlink($user_data);
			$filenames = glob(PSHB_PATH . '/feeds/*/' . $username . '.txt');
			if (!empty($filenames)) {
				array_map('unlink', $filenames);
			}
		}
		return (bool)$ok;
	}

	/**
	 * This action validates an email address, based on the token sent by email.
	 * It also serves the main page when user is blocked.
	 *
	 * Request parameters are:
	 *   - username
	 *   - token
	 *
	 * This route works with GET requests since the URL is provided by email.
	 * The security risks (e.g. forged URL by an attacker) are not very high so
	 * it’s ok.
	 *
	 * It returns 404 error if `force_email_validation` is disabled or if the
	 * user doesn’t exist.
	 *
	 * It returns 403 if user isn’t logged in and `username` param isn’t passed.
	 */
	public function validateEmailAction(): void {
		if (!FreshRSS_Context::systemConf()->force_email_validation) {
			Minz_Error::error(404);
		}

		FreshRSS_View::prependTitle(_t('user.email.validation.title') . ' · ');

		$username = Minz_Request::paramString('username');
		if (FreshRSS_Auth::hasAccess()) {
			$username = Minz_User::name() ?? '';
		}
		$token = Minz_Request::paramString('token');

		if ($username !== '') {
			$user_config = FreshRSS_UserConfiguration::getForUser($username);
		} elseif (FreshRSS_Auth::hasAccess()) {
			$user_config = FreshRSS_Context::userConf();
		} else {
			Minz_Error::error(403);
			return;
		}

		if (!FreshRSS_UserDAO::exists($username) || $user_config === null) {
			Minz_Error::error(404);
			return;
		}

		if ($user_config->email_validation_token === '') {
			Minz_Request::good(
				_t('user.email.validation.feedback.unnecessary'),
				['c' => 'index', 'a' => 'index'],
				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
			);
		}

		if ($token != '') {
			if (!hash_equals($user_config->email_validation_token, $token)) {
				Minz_Request::bad(
					_t('user.email.validation.feedback.wrong_token'),
					['c' => 'user', 'a' => 'validateEmail']
				);
			}

			$user_config->email_validation_token = '';
			if ($user_config->save()) {
				Minz_Request::good(
					_t('user.email.validation.feedback.ok'),
					['c' => 'index', 'a' => 'index'],
					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
				);
			} else {
				Minz_Request::bad(
					_t('user.email.validation.feedback.error'),
					['c' => 'user', 'a' => 'validateEmail']
				);
			}
		}
	}

	/**
	 * This action resends a validation email to the current user.
	 *
	 * It only acts on POST requests but doesn’t require any param (except the
	 * CSRF token).
	 *
	 * It returns 403 error if the user is not logged in or 404 if request is
	 * not POST. Else it redirects silently to the index if user has already
	 * validated its email, or to the user#validateEmail route.
	 */
	public function sendValidationEmailAction(): void {
		if (!FreshRSS_Auth::hasAccess()) {
			Minz_Error::error(403);
		}

		if (!Minz_Request::isPost()) {
			Minz_Error::error(404);
		}

		$username = Minz_User::name();

		if (FreshRSS_Context::userConf()->email_validation_token === '') {
			Minz_Request::forward([
				'c' => 'index',
				'a' => 'index',
			], true);
		}

		$mailer = new FreshRSS_User_Mailer();
		$ok = $username != null && $mailer->send_email_need_validation($username, FreshRSS_Context::userConf());

		$redirect_url = ['c' => 'user', 'a' => 'validateEmail'];
		if ($ok) {
			Minz_Request::good(
				_t('user.email.validation.feedback.email_sent'),
				$redirect_url,
				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
			);
		} else {
			Minz_Request::bad(
				_t('user.email.validation.feedback.email_failed'),
				$redirect_url
			);
		}
	}

	/**
	 * This action delete an existing user.
	 *
	 * Request parameter is:
	 *   - username
	 *
	 * @todo clean up this method. Idea: create a User->clean() method.
	 */
	public function deleteAction(): void {
		$username = Minz_Request::paramString('username');
		$self_deletion = Minz_User::name() === $username;

		if (!FreshRSS_Auth::hasAccess('admin') && !$self_deletion) {
			Minz_Error::error(403);
		}

		$redirect_url = ['c' => 'user', 'a' => 'manage'];

		if (Minz_Request::isPost()) {
			$ok = true;
			if ($self_deletion) {
				// We check the password if it’s a self-destruction
				$nonce = Minz_Session::paramString('nonce');
				$challenge = Minz_Request::paramString('challenge');

				$ok &= FreshRSS_FormAuth::checkCredentials(
					$username, FreshRSS_Context::userConf()->passwordHash,
					$nonce, $challenge
				);
				if (!$ok) {
					Minz_Request::bad(_t('feedback.auth.login.invalid'), ['c' => 'user', 'a' => 'profile']);
					return;
				}
			} elseif (self::reauthRedirect()) {
				return;
			}

			$ok &= self::deleteUser($username);

			if ($ok && $self_deletion) {
				FreshRSS_Auth::removeAccess();
				$redirect_url = ['c' => 'index', 'a' => 'index'];
			}
			invalidateHttpCache();

			if ($ok) {
				Minz_Request::setGoodNotification(_t('feedback.user.deleted', $username));
			} else {
				Minz_Request::setBadNotification(_t('feedback.user.deleted.error', $username));
			}
		}

		Minz_Request::forward($redirect_url, true);
	}

	public function promoteAction(): void {
		$this->toggleAction('is_admin', true);
	}

	public function demoteAction(): void {
		$this->toggleAction('is_admin', false);
	}

	public function enableAction(): void {
		$this->toggleAction('enabled', true);
	}

	public function disableAction(): void {
		$this->toggleAction('enabled', false);
	}

	private function toggleAction(string $field, bool $value): void {
		if (!FreshRSS_Auth::hasAccess('admin')) {
			Minz_Error::error(403);
		}

		if (!Minz_Request::isPost()) {
			Minz_Error::error(403);
		}

		if (self::reauthRedirect()) {
			return;
		}

		$username = Minz_Request::paramString('username');
		if (!FreshRSS_UserDAO::exists($username)) {
			Minz_Error::error(404);
		}

		if (null === $userConfig = FreshRSS_UserConfiguration::getForUser($username)) {
			Minz_Error::error(500);
			return;
		}

		if ($field === '') {
			Minz_Error::error(400, 'Invalid field name');
			return;
		}

		$userConfig->_attribute($field, $value);

		$ok = $userConfig->save();
		FreshRSS_UserDAO::touch($username);

		if ($ok) {
			Minz_Request::good(
				_t('feedback.user.updated', $username),
				['c' => 'user', 'a' => 'manage'],
				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
			);
		} else {
			Minz_Request::bad(
				_t('feedback.user.updated.error', $username),
				['c' => 'user', 'a' => 'manage']
			);
		}
	}

	public function detailsAction(): void {
		if (!FreshRSS_Auth::hasAccess('admin')) {
			Minz_Error::error(403);
		}

		if (self::reauthRedirect()) {
			return;
		}

		$username = Minz_Request::paramString('username');
		if (!FreshRSS_UserDAO::exists($username)) {
			Minz_Error::error(404);
		}

		if (Minz_Request::paramBoolean('ajax')) {
			$this->view->_layout(null);
		}

		$this->view->username = $username;
		$this->view->details = $this->retrieveUserDetails($username);
		FreshRSS_View::prependTitle($username . ' · ' . _t('gen.menu.user_management') . ' · ');
	}

	/** @return array{feed_count:?int,article_count:?int,database_size:?int,language:string,mail_login:string,enabled:bool,is_admin:bool,last_user_activity:string,is_default:bool} */
	private function retrieveUserDetails(string $username, bool $fast = false): array {
		$feedDAO = $fast ? null : FreshRSS_Factory::createFeedDao($username);
		$entryDAO = $fast ? null : FreshRSS_Factory::createEntryDao($username);
		$databaseDAO = $fast ? null : FreshRSS_Factory::createDatabaseDAO($username);

		$userConfiguration = FreshRSS_UserConfiguration::getForUser($username);
		if ($userConfiguration === null) {
			throw new Exception('Error loading user configuration!');
		}

		return [
			'feed_count' => isset($feedDAO) ? $feedDAO->count() : null,
			'article_count' => isset($entryDAO) ? $entryDAO->count() : null,
			'database_size' => isset($databaseDAO) ? $databaseDAO->size() : null,
			'language' => $userConfiguration->language,
			'mail_login' => $userConfiguration->mail_login,
			'enabled' => $userConfiguration->enabled,
			'is_admin' => $userConfiguration->is_admin,
			'last_user_activity' => date('c', FreshRSS_UserDAO::mtime($username)) ?: '',
			'is_default' => FreshRSS_Context::systemConf()->default_user === $username,
		];
	}
}
