<?php

declare(strict_types=1);


/**
 * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */


namespace OCA\Circles\FederatedItems;

use OC\User\NoUserException;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\FederatedItemBadRequestException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedItemNotFoundException;
use OCA\Circles\Exceptions\FederatedItemRemoteException;
use OCA\Circles\Exceptions\FederatedItemServerException;
use OCA\Circles\Exceptions\FederatedUserException;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Exceptions\InvalidIdException;
use OCA\Circles\Exceptions\MemberNotFoundException;
use OCA\Circles\Exceptions\MembersLimitException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RemoteInstanceException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\SingleCircleNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\Exceptions\UserTypeNotFoundException;
use OCA\Circles\IFederatedItem;
use OCA\Circles\IFederatedItemAsyncProcess;
use OCA\Circles\IFederatedItemHighSeverity;
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
use OCA\Circles\IFederatedItemMemberRequired;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Federated\RemoteInstance;
use OCA\Circles\Model\Helpers\MemberHelper;
use OCA\Circles\Model\ManagedModel;
use OCA\Circles\Model\Member;
use OCA\Circles\Service\CircleService;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\EventService;
use OCA\Circles\Service\FederatedUserService;
use OCA\Circles\Service\MaintenanceService;
use OCA\Circles\Service\MemberService;
use OCA\Circles\Service\MembershipService;
use OCA\Circles\Service\RemoteStreamService;
use OCA\Circles\StatusCode;
use OCA\Circles\Tools\Traits\TDeserialize;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\IUserManager;

/**
 * Class SingleMemberAdd
 *
 * @package OCA\Circles\FederatedItems
 */
class SingleMemberAdd implements
	IFederatedItem,
	IFederatedItemAsyncProcess,
	IFederatedItemHighSeverity,
	IFederatedItemMemberRequired,
	IFederatedItemMemberCheckNotRequired {
	use TDeserialize;
	use TStringTools;
	use TNCLogger;

	public function __construct(
		protected IUserManager $userManager,
		protected MemberRequest $memberRequest,
		protected FederatedUserService $federatedUserService,
		protected RemoteStreamService $remoteStreamService,
		protected CircleService $circleService,
		protected MemberService $memberService,
		protected MembershipService $membershipService,
		protected EventService $eventService,
		protected ConfigService $configService,
		protected MaintenanceService $maintenanceService,
	) {
	}


	/**
	 * @param FederatedEvent $event
	 *
	 * @throws FederatedItemBadRequestException
	 * @throws FederatedItemNotFoundException
	 * @throws FederatedItemServerException
	 * @throws FederatedItemRemoteException
	 * @throws FederatedItemException
	 * @throws RequestBuilderException
	 */
	public function verify(FederatedEvent $event): void {
		$member = $event->getMember();
		$circle = $event->getCircle();
		$initiator = $circle->getInitiator();

		$initiatorHelper = new MemberHelper($initiator);
		if (!$circle->isConfig(Circle::CFG_FRIEND)) {
			$initiatorHelper->mustBeModerator();
		}

		$member = $this->generateMember($event, $circle, $member);

		$event->setMembers([$member]);
		$event->setOutcome($this->serialize($member));

		$this->eventService->memberPreparing($event);
	}


	/**
	 * @param FederatedEvent $event
	 *
	 * @throws InvalidIdException
	 * @throws RemoteNotFoundException
	 * @throws RequestBuilderException
	 * @throws UnknownRemoteException
	 */
	public function manage(FederatedEvent $event): void {
		$member = $event->getMember();
		if (!$this->memberService->insertOrUpdate($member)) {
			return;
		}

		if ($member->getStatus() === Member::STATUS_INVITED) {
			$this->eventService->memberInviting($event);
		} else {
			$this->eventService->memberAdding($event);
		}

		$this->membershipService->updatePopulation($event->getCircle());
	}


	/**
	 * @param FederatedEvent $event
	 * @param array $results
	 */
	public function result(FederatedEvent $event, array $results): void {
		$member = $event->getMember();
		if ($member->getStatus() === Member::STATUS_INVITED) {
			$this->eventService->memberInvited($event, $results);
		} else {
			$this->eventService->memberAdded($event, $results);
		}
	}


	/**
	 * @param FederatedEvent $event
	 * @param Circle $circle
	 * @param Member $member
	 *
	 * @return Member
	 * @throws CircleNotFoundException
	 * @throws FederatedItemBadRequestException
	 * @throws FederatedItemException
	 * @throws FederatedUserException
	 * @throws FederatedUserNotFoundException
	 * @throws InvalidIdException
	 * @throws MembersLimitException
	 * @throws OwnerNotFoundException
	 * @throws RemoteInstanceException
	 * @throws RemoteNotFoundException
	 * @throws RemoteResourceNotFoundException
	 * @throws SingleCircleNotFoundException
	 * @throws UnknownRemoteException
	 * @throws UserTypeNotFoundException
	 * @throws RequestBuilderException
	 */
	protected function generateMember(FederatedEvent $event, Circle $circle, Member $member): Member {
		try {
			if ($member->getSingleId() !== '') {
				$userId = $member->getSingleId() . '@' . $member->getInstance();
				$federatedUser = $this->federatedUserService->getFederatedUser($userId, Member::TYPE_SINGLE);
			} else {
				$userId = $member->getUserId() . '@' . $member->getInstance();
				$federatedUser = $this->federatedUserService->getFederatedUser(
					$userId,
					$member->getUserType()
				);
			}
		} catch (MemberNotFoundException $e) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[120], 120);
		}

		$allowedTypes = $this->configService->getAppValueInt(ConfigService::ALLOWED_TYPES);
		if ($federatedUser->getUserType() < Member::TYPE_APP
			&& ($allowedTypes & $federatedUser->getUserType()) === 0) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[132], 132);
		}

		if ($federatedUser->getBasedOn()->isConfig(Circle::CFG_ROOT)) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[125], 125);
		}

		if ($circle->isConfig(Circle::CFG_LOCAL)
			&& $federatedUser->getUserType() === Member::TYPE_CIRCLE
			&& !$federatedUser->getBasedOn()->isConfig(Circle::CFG_LOCAL)) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[131], 131);
		}

		if ($member->getSingleId() === $circle->getSingleId()) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[128], 128);
		}

		if (!$this->configService->isLocalInstance($member->getInstance())) {
			if ($circle->isConfig(Circle::CFG_LOCAL)) {
				throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[126], 126);
			}

			if (!$circle->isConfig(Circle::CFG_FEDERATED)) {
				$remoteInstance = $this->remoteStreamService->getCachedRemoteInstance($member->getInstance());
				if ($remoteInstance->getType() !== RemoteInstance::TYPE_GLOBALSCALE) {
					throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[127], 127);
				}
			}
		}

		$member->importFromIFederatedUser($federatedUser);
		$member->setCircleId($circle->getSingleId());
		$member->setCircle($circle);

		$this->confirmPatron($event, $member);
		$this->manageMemberStatus($circle, $member);

		$this->circleService->confirmCircleNotFull($circle);

		// The idea is that adding the member during the self::verify() will help during the broadcasting
		// of the event to Federated RemoteInstance for their first member.
		$this->memberRequest->insertOrUpdate($member);

		try {
			$displayName = $this->maintenanceService->updateDisplayName($member);
			if ($displayName !== '') {
				$member->setDisplayName($displayName);
			}
		} catch (NoUserException) {
			// ignoreable
		}

		return $member;
	}


	/**
	 * @param Circle $circle
	 * @param Member $member
	 *
	 * @throws FederatedItemBadRequestException
	 * @throws RequestBuilderException
	 */
	private function manageMemberStatus(Circle $circle, Member $member) {
		try {
			$knownMember = $this->memberRequest->searchMember($member);
			$member->setId($knownMember->getId());

			if ($knownMember->getLevel() === Member::LEVEL_NONE) {
				switch ($knownMember->getStatus()) {
					case Member::STATUS_BLOCKED:
						if ($circle->isConfig(Circle::CFG_INVITE)) {
							$member->setStatus(Member::STATUS_INVITED);
						}

						return;

					case Member::STATUS_REQUEST:
						$member->setLevel(Member::LEVEL_MEMBER);
						$member->setStatus(Member::STATUS_MEMBER);

						return;

					case Member::STATUS_INVITED:
						throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[123], 123);
				}
			}

			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[122], 122);
		} catch (MemberNotFoundException $e) {
			$member->setId($this->token(ManagedModel::ID_LENGTH));

			if ($circle->isConfig(Circle::CFG_INVITE)
				&& $member->getUserType() !== Member::TYPE_MAIL
				&& $member->getUserType() !== Member::TYPE_CONTACT) {
				$member->setStatus(Member::STATUS_INVITED);
			} else {
				$member->setLevel(Member::LEVEL_MEMBER);
				$member->setStatus(Member::STATUS_MEMBER);
			}
		}
	}


	/**
	 * @param FederatedEvent $event
	 * @param Member $member
	 *
	 * @throws FederatedItemBadRequestException
	 * @throws FederatedUserException
	 * @throws RemoteNotFoundException
	 * @throws RequestBuilderException
	 * @throws UnknownRemoteException
	 */
	private function confirmPatron(FederatedEvent $event, Member $member): void {
		if (!$member->hasInvitedBy()) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[129], 129);
		}

		$patron = $member->getInvitedBy();
		if ($patron->getInstance() !== $event->getSender()) {
			throw new FederatedItemBadRequestException(StatusCode::$MEMBER_ADD[130], 130);
		}

		$this->federatedUserService->confirmSingleIdUniqueness($patron);
	}
}
