<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OCA\DAV\CardDAV;

use OCA\DAV\DAV\Sharing\IShareable;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Backend\BackendInterface;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IMoveTarget;
use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;

/**
 * Class AddressBook
 *
 * @package OCA\DAV\CardDAV
 * @property CardDavBackend $carddavBackend
 */
class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMoveTarget {
	/**
	 * AddressBook constructor.
	 *
	 * @param BackendInterface $carddavBackend
	 * @param array $addressBookInfo
	 * @param IL10N $l10n
	 */
	public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n) {
		parent::__construct($carddavBackend, $addressBookInfo);


		if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME
			&& $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) {
			$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Contacts');
		}
	}

	/**
	 * Updates the list of shares.
	 *
	 * The first array is a list of people that are to be added to the
	 * addressbook.
	 *
	 * Every element in the add array has the following properties:
	 *   * href - A url. Usually a mailto: address
	 *   * commonName - Usually a first and last name, or false
	 *   * readOnly - A boolean value
	 *
	 * Every element in the remove array is just the address string.
	 *
	 * @param list<array{href: string, commonName: string, readOnly: bool}> $add
	 * @param list<string> $remove
	 * @throws Forbidden
	 */
	#[\Override]
	public function updateShares(array $add, array $remove): void {
		if ($this->isShared()) {
			throw new Forbidden();
		}
		$this->carddavBackend->updateShares($this, $add, $remove);
	}

	/**
	 * Returns the list of people whom this addressbook is shared with.
	 *
	 * Every element in this array should have the following properties:
	 *   * href - Often a mailto: address
	 *   * commonName - Optional, for example a first + last name
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
	 *   * readOnly - boolean
	 *
	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
	 */
	#[\Override]
	public function getShares(): array {
		if ($this->isShared()) {
			return [];
		}
		return $this->carddavBackend->getShares($this->getResourceId());
	}

	#[\Override]
	public function getACL() {
		$acl = [
			[
				'privilege' => '{DAV:}read',
				'principal' => $this->getOwner(),
				'protected' => true,
			],[
				'privilege' => '{DAV:}write',
				'principal' => $this->getOwner(),
				'protected' => true,
			],
			[
				'privilege' => '{DAV:}write-properties',
				'principal' => $this->getOwner(),
				'protected' => true,
			],
		];

		if ($this->getOwner() === 'principals/system/system') {
			$acl[] = [
				'privilege' => '{DAV:}read',
				'principal' => '{DAV:}authenticated',
				'protected' => true,
			];
			$acl[] = [
				'privilege' => '{DAV:}write-properties',
				'principal' => '{DAV:}authenticated',
				'protected' => true,
			];
		}

		if (!$this->isShared()) {
			return $acl;
		}

		if ($this->getOwner() !== parent::getOwner()) {
			$acl[] = [
				'privilege' => '{DAV:}read',
				'principal' => parent::getOwner(),
				'protected' => true,
			];
			if ($this->canWrite()) {
				$acl[] = [
					'privilege' => '{DAV:}write',
					'principal' => parent::getOwner(),
					'protected' => true,
				];
			}
		}

		$acl = $this->carddavBackend->applyShareAcl($this->getResourceId(), $acl);
		$allowedPrincipals = [$this->getOwner(), parent::getOwner(), 'principals/system/system', '{DAV:}authenticated'];
		return array_filter($acl, function ($rule) use ($allowedPrincipals) {
			return \in_array($rule['principal'], $allowedPrincipals, true);
		});
	}

	#[\Override]
	public function getChildACL() {
		return $this->getACL();
	}

	#[\Override]
	public function getChild($name) {
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
		if (!$obj) {
			throw new NotFound('Card not found');
		}
		$obj['acl'] = $this->getChildACL();
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
	}

	#[\Override]
	public function getChildren() {
		$objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
		$children = [];
		foreach ($objs as $obj) {
			$obj['acl'] = $this->getChildACL();
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
		}

		return $children;
	}

	#[\Override]
	public function getMultipleChildren(array $paths) {
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
		$children = [];
		foreach ($objs as $obj) {
			$obj['acl'] = $this->getChildACL();
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
		}

		return $children;
	}

	#[\Override]
	public function getResourceId(): int {
		return $this->addressBookInfo['id'];
	}

	#[\Override]
	public function getOwner(): ?string {
		if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
			return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'];
		}
		return parent::getOwner();
	}

	#[\Override]
	public function delete() {
		if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
			$principal = 'principal:' . parent::getOwner();
			$shares = $this->carddavBackend->getShares($this->getResourceId());
			$shares = array_filter($shares, function ($share) use ($principal) {
				return $share['href'] === $principal;
			});
			if (empty($shares)) {
				throw new Forbidden();
			}

			$this->carddavBackend->updateShares($this, [], [
				$principal
			]);
			return;
		}
		parent::delete();
	}

	#[\Override]
	public function propPatch(PropPatch $propPatch) {
		if (!isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
			parent::propPatch($propPatch);
		}
	}

	public function getContactsGroups() {
		return $this->carddavBackend->collectCardProperties($this->getResourceId(), 'CATEGORIES');
	}

	private function isShared(): bool {
		if (!isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
			return false;
		}

		return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'] !== $this->addressBookInfo['principaluri'];
	}

	private function canWrite(): bool {
		if (isset($this->addressBookInfo['{http://owncloud.org/ns}read-only'])) {
			return !$this->addressBookInfo['{http://owncloud.org/ns}read-only'];
		}
		return true;
	}

	#[\Override]
	public function getChanges($syncToken, $syncLevel, $limit = null) {

		return parent::getChanges($syncToken, $syncLevel, $limit);
	}

	/**
	 * @inheritDoc
	 */
	#[\Override]
	public function moveInto($targetName, $sourcePath, INode $sourceNode) {
		if (!($sourceNode instanceof Card)) {
			return false;
		}

		try {
			return $this->carddavBackend->moveCard(
				$sourceNode->getAddressbookId(),
				$sourceNode->getUri(),
				$this->getResourceId(),
				$targetName,
			);
		} catch (Exception $e) {
			// Avoid injecting LoggerInterface everywhere
			Server::get(LoggerInterface::class)->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
			return false;
		}
	}
}
