<?php

declare(strict_types=1);

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

namespace OCA\DAV\CalDAV\Federation;

use OCA\DAV\CalDAV\Federation\Protocol\CalendarFederationProtocolV1;
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\DAV\Sharing\SharingMapper;
use OCP\AppFramework\Http;
use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\OCM\Exceptions\OCMProviderException;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Calendar;

// TODO: Convert this to an abstract service like the addressbook/calendar sharing services once we
//       support addressbook federation as well.
class FederationSharingService {
	public function __construct(
		private readonly ICloudFederationProviderManager $federationManager,
		private readonly ICloudFederationFactory $federationFactory,
		private readonly IUserManager $userManager,
		private readonly IURLGenerator $url,
		private readonly LoggerInterface $logger,
		private readonly ISecureRandom $random,
		private readonly SharingMapper $sharingMapper,
	) {
	}

	/**
	 * Decode a (base64) encoded remote user principal and return the remote user's cloud id. Will
	 * return null if the given principal is not belonging to a remote user (or has an invalid
	 * format).
	 *
	 * The remote user/cloud id needs to be encoded as it might contain slashes.
	 */
	private function decodeRemoteUserPrincipal(string $principal): ?string {
		// Expected format: principals/remote-users/abcdef123
		if (!str_starts_with($principal, 'principals/remote-users/')) {
			return null;
		}
		$encodedId = substr($principal, strlen('principals/remote-users/'));

		$decodedId = base64_decode($encodedId);
		if (!is_string($decodedId)) {
			return null;
		}

		return $decodedId;
	}

	/**
	 * Send a calendar share to a remote instance and create a federated share locally if it is
	 * accepted.
	 *
	 * @param IShareable $shareable The calendar to be shared.
	 * @param string $principal The principal to share with (should be a remote user principal).
	 * @param int $access The access level. The remote serve might reject it.
	 */
	public function shareWith(IShareable $shareable, string $principal, int $access): void {
		$baseError = 'Failed to create federated calendar share: ';

		// 1. Validate share data
		$shareWith = $this->decodeRemoteUserPrincipal($principal);
		if ($shareWith === null) {
			$this->logger->error($baseError . 'Principal of sharee is not belonging to a remote user', [
				'shareable' => $shareable->getName(),
				'encodedShareWith' => $principal,
			]);
			return;
		}

		[,, $ownerUid] = explode('/', $shareable->getOwner());
		$owner = $this->userManager->get($ownerUid);
		if ($owner === null) {
			$this->logger->error($baseError . 'Shareable is not owned by a user on this server', [
				'shareable' => $shareable->getName(),
				'shareWith' => $shareWith,
			]);
			return;
		}

		// Need a calendar instance to extract properties for the protocol
		$calendar = $shareable;
		if (!($calendar instanceof Calendar)) {
			$this->logger->error($baseError . 'Shareable is not a calendar', [
				'shareable' => $shareable->getName(),
				'owner' => $owner,
				'shareWith' => $shareWith,
			]);
			return;
		}

		$getProp = static fn (string $prop) => $calendar->getProperties([$prop])[$prop] ?? null;

		$displayName = $getProp('{DAV:}displayname') ?? '';

		$token = $this->random->generate(32);
		$share = $this->federationFactory->getCloudFederationShare(
			$shareWith,
			$shareable->getName(),
			$displayName,
			CalendarFederationProvider::PROVIDER_ID,
			// Resharing is not possible so the owner is always the sharer
			$owner->getCloudId(),
			$owner->getDisplayName(),
			$owner->getCloudId(),
			$owner->getDisplayName(),
			$token,
			CalendarFederationProvider::USER_SHARE_TYPE,
			CalendarFederationProvider::CALENDAR_RESOURCE,
		);

		// 2. Send share to federated instance
		$shareWithEncoded = base64_encode($shareWith);
		$relativeCalendarUrl = "remote-calendars/$shareWithEncoded/" . $calendar->getName() . '_shared_by_' . $ownerUid;
		$calendarUrl = $this->url->linkTo('', 'remote.php') . "/dav/$relativeCalendarUrl";
		$calendarUrl = $this->url->getAbsoluteURL($calendarUrl);
		$protocol = new CalendarFederationProtocolV1();
		$protocol->setUrl($calendarUrl);
		$protocol->setDisplayName($displayName);
		$protocol->setColor($getProp('{http://apple.com/ns/ical/}calendar-color'));
		$protocol->setAccess($access);
		$protocol->setComponents(implode(',', $getProp(
			'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set')?->getValue() ?? [],
		));
		$share->setProtocol([
			// Preserve original protocol contents
			...$share->getProtocol(),
			...$protocol->toProtocol(),
		]);

		try {
			$response = $this->federationManager->sendCloudShare($share);
		} catch (OCMProviderException $e) {
			$this->logger->error($baseError . $e->getMessage(), [
				'exception' => $e,
				'owner' => $owner->getUID(),
				'calendar' => $shareable->getName(),
				'shareWith' => $shareWith,
			]);
			return;
		}

		if ($response->getStatusCode() !== Http::STATUS_CREATED) {
			$this->logger->error($baseError . 'Server replied with code ' . $response->getStatusCode(), [
				'responseBody' => $response->getBody(),
				'owner' => $owner->getUID(),
				'calendar' => $shareable->getName(),
				'shareWith' => $shareWith,
			]);
			return;
		}

		// 3. Create a local DAV share to track the token for authentication
		$shareWithPrincipalUri = RemoteUserPrincipalBackend::PRINCIPAL_PREFIX . '/' . $shareWithEncoded;
		$this->sharingMapper->deleteShare(
			$shareable->getResourceId(),
			'calendar',
			$shareWithPrincipalUri,
		);
		$this->sharingMapper->shareWithToken(
			$shareable->getResourceId(),
			'calendar',
			$access,
			$shareWithPrincipalUri,
			$token,
		);
	}
}
