<?php

declare(strict_types=1);
/**
 * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\Calendar\Controller;

use Exception;
use OCA\Calendar\Service\ContactsService;
use OCA\Calendar\Service\ServiceException;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\QueryException;
use OCP\Contacts\IManager;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

/**
 * Class ContactController
 *
 * @package OCA\Calendar\Controller
 */
class ContactController extends Controller {
	/**
	 * ContactController constructor.
	 *
	 * @param string $appName
	 * @param IRequest $request
	 */
	public function __construct(
		string $appName,
		IRequest $request,
		private IManager $contactsManager,
		private IAppManager $appManager,
		private IUserManager $userManager,
		private ContactsService $contactsService,
		private IAppConfig $appConfig,
		private IConfig $config,
		private IGroupManager $groupManager,
		private LoggerInterface $logger,
		private ?string $userId,
	) {
		parent::__construct($appName, $request);
	}

	/**
	 * Search for a location based on a contact's name or address
	 *
	 * @param string $search Name or address to search for
	 * @return JSONResponse
	 *
	 * @NoAdminRequired
	 */
	public function searchLocation(string $search): JSONResponse {
		if (!$this->contactsManager->isEnabled()) {
			return new JSONResponse();
		}

		$result = $this->contactsManager->search($search, ['FN', 'ADR'], ['enumeration' => false]);

		$contacts = [];
		foreach ($result as $r) {
			// Information about system users is fetched via DAV nowadays
			if ($this->contactsService->isSystemBook($r)) {
				continue;
			}

			if (!isset($r['ADR'])) {
				continue;
			}

			$name = $this->contactsService->getNameFromContact($r);
			$photo = $this->contactsService->getPhotoUri($r);
			$addresses = $this->contactsService->getAddress($r);

			$contacts[] = [
				'name' => $name,
				'addresses' => $addresses,
				'photo' => $photo,
			];
		}

		return new JSONResponse($contacts);
	}

	/**
	 * Search for a contact based on a contact's name or email-address
	 *
	 * @param string $search Name or email to search for
	 * @return JSONResponse
	 *
	 * @NoAdminRequired
	 */
	public function searchAttendee(string $search):JSONResponse {
		if (!$this->contactsManager->isEnabled()) {
			return new JSONResponse();
		}

		// Read restriction configuration
		$externalAttendeesDisabled = $this->appConfig->getValueBool('dav', 'caldav_external_attendees_disabled', false);
		$shareeGroupsOnlyRestriction = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
		$shareeGroupsOnlyExclusion = [];
		$shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
		$shareeEnumerationInGroupOnly = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
		$shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
		$shareeEnumerationFullMatchUserId = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes';
		$shareeEnumerationFullMatchEmail = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
		if ($shareeGroupsOnlyRestriction) {
			$excludedGroups = json_decode(
				$this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', '[]'),
				true
			);
			if (is_array($excludedGroups)) {
				$shareeGroupsOnlyExclusion = $excludedGroups;
			}
		}

		$result = $this->contactsManager->search(
			$search,
			['UID', 'FN', 'EMAIL'],
			[
				'enumeration' => $shareeEnumeration,
				'fullmatch' => $shareeEnumerationFullMatch,
			]
		);

		// Get user groups if group restriction is enabled
		$userGroups = [];
		if (($shareeGroupsOnlyRestriction || ($shareeEnumeration && $shareeEnumerationInGroupOnly)) && $this->userId !== null) {
			$user = $this->userManager->get($this->userId);
			if ($user === null) {
				return new JSONResponse();
			}
			$userGroups = $this->groupManager->getUserGroupIds($user);

			if ($shareeGroupsOnlyRestriction && $shareeGroupsOnlyExclusion !== []
				&& array_intersect($userGroups, $shareeGroupsOnlyExclusion) !== []) {
				$shareeGroupsOnlyRestriction = false;
			}
		}

		$contacts = [];
		foreach ($result as $r) {
			if (!$this->contactsService->hasEmail($r)) {
				continue;
			}

			$isSystemUser = $this->contactsService->isSystemBook($r);

			// When external attendees are disabled, only include system book contacts
			if ($externalAttendeesDisabled && !$isSystemUser) {
				continue;
			}

			// Apply group restrictions for system users
			$isInSameGroup = false;
			if ($isSystemUser && ($shareeGroupsOnlyRestriction || $shareeEnumerationInGroupOnly)) {
				foreach ($userGroups as $userGroup) {
					if ($this->groupManager->isInGroup($r['UID'], $userGroup)) {
						$isInSameGroup = true;
						break;
					}
				}
				if ($shareeGroupsOnlyRestriction && !$isInSameGroup) {
					continue;
				}
				if (!$shareeEnumerationFullMatch && !$isInSameGroup) {
					continue;
				}
			}

			$name = $this->contactsService->getNameFromContact($r);
			$email = $this->contactsService->getEmail($r);
			$photo = $this->contactsService->getPhotoUri($r);
			$timezoneId = $this->contactsService->getTimezoneId($r);
			$lang = $this->contactsService->getLanguageId($r);

			// Check full match requirements for system users not in same group
			if ($isSystemUser && $shareeEnumerationInGroupOnly && !$isInSameGroup) {
				$lowerSearch = strtolower($search);
				$matchFound = false;

				// Check for full match on name, user ID, or email
				if ($lowerSearch !== '') {
					if ($shareeEnumerationFullMatch && !empty($name) && $lowerSearch === strtolower($name)) {
						$matchFound = true;
					}
					if ($shareeEnumerationFullMatchUserId && $lowerSearch === strtolower($r['UID'])) {
						$matchFound = true;
					}
					if ($shareeEnumerationFullMatchEmail && $email) {
						foreach ($email as $e) {
							if ($lowerSearch === strtolower($e)) {
								$matchFound = true;
								break;
							}
						}
					}
				}

				if (!$matchFound) {
					continue;
				}
			}

			$contacts[] = [
				'name' => $name,
				'emails' => $email,
				'lang' => $lang,
				'tzid' => $timezoneId,
				'photo' => $photo,
				'type' => 'individual',
				'source' => $isSystemUser ? 'system' : 'user',
			];
		}

		// Skip contact groups when external attendees are disabled
		if (!$externalAttendeesDisabled) {
			$groups = $this->contactsManager->search($search, ['CATEGORIES']);
			$groups = array_filter($groups, function ($group) {
				return $this->contactsService->hasEmail($group);
			});
			$filtered = $this->contactsService->filterGroupsWithCount($groups, $search);
			foreach ($filtered as $groupName => $count) {
				if ($count === 0) {
					continue;
				}
				$contacts[] = [
					'name' => $groupName,
					'emails' => ['mailto:' . urlencode($groupName) . '@group'],
					'lang' => '',
					'tzid' => '',
					'photo' => '',
					'type' => 'contactsgroup',
					'source' => 'user',
					'members' => $count,
				];
			}
		}

		return new JSONResponse($contacts);
	}

	#[NoAdminRequired]
	public function getContactGroupMembers(string $groupName): JSONResponse {
		if (!$this->contactsManager->isEnabled()) {
			return new JSONResponse();
		}

		$groupmembers = $this->contactsManager->search($groupName, ['CATEGORIES'], ['enumeration' => false]);
		$contacts = [];
		foreach ($groupmembers as $r) {
			if (!in_array($groupName, explode(',', $r['CATEGORIES']), true)) {
				continue;
			}
			if (!$this->contactsService->hasEmail($r) || $this->contactsService->isSystemBook($r)) {
				continue;
			}
			$name = $this->contactsService->getNameFromContact($r);
			$email = $this->contactsService->getEmail($r);
			$photo = $this->contactsService->getPhotoUri($r);
			$timezoneId = $this->contactsService->getTimezoneId($r);
			$lang = $this->contactsService->getLanguageId($r);
			$contacts[] = [
				'commonName' => $name,
				'email' => $email[0],
				'calendarUserType' => 'INDIVIDUAL',
				'language' => $lang,
				'timezoneId' => $timezoneId,
				'avatar' => $photo,
				'isUser' => false,
				'member' => 'mailto:' . urlencode($groupName) . '@group',
			];
		}
		return new JSONResponse($contacts);
	}

	/**
	 * Query members of a circle by circleId
	 *
	 * @param string $circleId CircleId to query for members
	 * @return JSONResponse
	 * @throws Exception
	 * @throws \OCP\AppFramework\QueryException
	 *
	 * @NoAdminRequired
	 */
	public function getCircleMembers(string $circleId):JSONResponse {
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
			return new JSONResponse();
		}
		if (!$this->contactsManager->isEnabled()) {
			return new JSONResponse();
		}

		try {
			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleId, true);
		} catch (QueryException $ex) {
			return new JSONResponse();
		} catch (CircleNotFoundException $ex) {
			return new JSONResponse();
		}

		if (!$circle) {
			return new JSONResponse();
		}

		$circleMembers = $circle->getInheritedMembers();

		foreach ($circleMembers as $circleMember) {
			if ($circleMember->isLocal()) {

				$circleMemberUserId = $circleMember->getUserId();

				$user = $this->userManager->get($circleMemberUserId);

				if ($user === null) {
					throw new ServiceException('Could not find organizer');
				}

				$contacts[] = [
					'commonName' => $circleMember->getDisplayName(),
					'calendarUserType' => 'INDIVIDUAL',
					'email' => $user->getEMailAddress(),
					'isUser' => true,
					'avatar' => $circleMemberUserId,
					'hasMultipleEMails' => false,
					'dropdownName' => $circleMember->getDisplayName(),
					'member' => 'mailto:circle+' . $circleId . '@' . $circleMember->getInstance(),
				];
			}
		}

		return new JSONResponse($contacts);
	}

	/**
	 * Get a contact's photo based on their email-address
	 *
	 * @param string $search Exact email-address to match
	 * @return JSONResponse
	 *
	 * @NoAdminRequired
	 */
	public function searchPhoto(string $search):JSONResponse {
		if (!$this->contactsManager->isEnabled()) {
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
		}

		$result = $this->contactsManager->search($search, ['EMAIL'], ['enumeration' => false]);

		foreach ($result as $r) {
			if (!$this->contactsService->hasEmail($r) || $this->contactsService->isSystemBook($r)) {
				continue;
			}
			$email = $this->contactsService->getEmail($r);

			$match = false;
			foreach ($email as $e) {
				if ($e === $search) {
					$match = true;
				}
			}

			if (!$match) {
				continue;
			}

			$photo = $this->contactsService->getPhotoUri($r);
			if ($photo === null) {
				continue;
			}

			$name = $this->contactsService->getNameFromContact($r);

			return new JSONResponse([
				'name' => $name,
				'photo' => $photo,
			]);
		}

		return new JSONResponse([], Http::STATUS_NOT_FOUND);
	}

}
