<?php

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

namespace OCA\Files_Sharing;

use OC\Files\Filesystem;
use OC\Files\View;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\IUser;
use OCP\Share\Events\VerifyMountPointEvent;
use OCP\Share\IManager;
use OCP\Share\IShare;

/**
 * Validate that mount target is valid
 */
class ShareTargetValidator {
	private CappedMemoryCache $folderExistsCache;

	public function __construct(
		private readonly IManager $shareManager,
		private readonly IEventDispatcher $eventDispatcher,
		private readonly IRootFolder $rootFolder,
	) {
		$this->folderExistsCache = new CappedMemoryCache();
	}

	private function getViewForUser(IUser $user): View {
		/**
		 * @psalm-suppress InternalClass
		 * @psalm-suppress InternalMethod
		 */
		return new View('/' . $user->getUID() . '/files');
	}

	/**
	 * check if the parent folder exists otherwise move the mount point up
	 *
	 * @param callable(string):?ICachedMountInfo $getMountByPath
	 * @param IShare[] $childShares
	 * @return string
	 */
	public function verifyMountPoint(
		IUser $user,
		IShare &$share,
		callable $getMountByPath,
		array $childShares,
	): string {
		$mountPoint = basename($share->getTarget());
		$parent = dirname($share->getTarget());

		$recipientView = $this->getViewForUser($user);
		$event = new VerifyMountPointEvent($share, $recipientView, $parent, $user);
		$this->eventDispatcher->dispatchTyped($event);
		$parent = $event->getParent();

		/** @psalm-suppress InternalMethod */
		$absoluteParent = $recipientView->getAbsolutePath($parent);

		// the share target always has to be in the users home
		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
		$parentMount = $userFolder->getMountPoint();

		$cached = $this->folderExistsCache->get($parent);
		if ($cached !== null) {
			$parentExists = $cached;
		} else {
			$parentCache = $parentMount->getStorage()->getCache();
			$parentExists = $parentCache->inCache($parentMount->getInternalPath($absoluteParent));
			$this->folderExistsCache->set($parent, $parentExists);
		}
		if (!$parentExists) {
			if ($event->createParent()) {
				$internalPath = $parentMount->getInternalPath($absoluteParent);
				$parentMount->getStorage()->mkdir($internalPath);
				$parentMount->getStorage()->getUpdater()->update($internalPath);
			} else {
				$parent = Helper::getShareFolder($recipientView, $user->getUID());
				/** @psalm-suppress InternalMethod */
				$absoluteParent = $recipientView->getAbsolutePath($parent);
			}
		}

		$newAbsoluteMountPoint = $this->generateUniqueTarget(
			$share->getNodeId(),
			Filesystem::normalizePath($absoluteParent . '/' . $mountPoint),
			$parentMount,
			$getMountByPath,
		);

		/** @psalm-suppress InternalMethod */
		$newMountPoint = $recipientView->getRelativePath($newAbsoluteMountPoint);
		if ($newMountPoint === null) {
			return $share->getTarget();
		}

		if ($newMountPoint !== $share->getTarget()) {
			$this->updateFileTarget($user, $newMountPoint, $share, $childShares);
		}

		return $newMountPoint;
	}


	/**
	 * @param callable(string):?ICachedMountInfo $getMountByPath
	 */
	public function generateUniqueTarget(
		int $shareNodeId,
		string $absolutePath,
		IMountPoint $parentMount,
		callable $getMountByPath,
	): string {
		$pathInfo = pathinfo($absolutePath);
		$ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
		$name = $pathInfo['filename'];
		$dir = $pathInfo['dirname'];

		$i = 2;
		$parentCache = $parentMount->getStorage()->getCache();
		$internalPath = $parentMount->getInternalPath($absolutePath);
		while ($parentCache->inCache($internalPath) || $this->hasConflictingMount($shareNodeId, $getMountByPath, $absolutePath)) {
			$absolutePath = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
			$internalPath = $parentMount->getInternalPath($absolutePath);
			$i++;
		}

		return $absolutePath;
	}

	/**
	 * @param callable(string):?ICachedMountInfo $getMountByPath
	 */
	private function hasConflictingMount(int $shareNodeId, callable $getMountByPath, string $absolutePath): bool {
		$mount = $getMountByPath($absolutePath . '/');
		if ($mount === null) {
			return false;
		}

		if ($mount->getMountProvider() === MountProvider::class && $mount->getRootId() === $shareNodeId) {
			// "conflicting" mount is a mount for the current share
			return false;
		}

		return true;
	}

	/**
	 * update fileTarget in the database if the mount point changed
	 *
	 * @param IShare[] $childShares
	 */
	private function updateFileTarget(IUser $user, string $newPath, IShare &$share, array $childShares) {
		$share->setTarget($newPath);

		foreach ($childShares as $tmpShare) {
			$tmpShare->setTarget($newPath);
			$this->shareManager->moveShare($tmpShare, $user->getUID());
		}
	}
}
