<?php

namespace XF\Repository;

use XF\Bookmark\AbstractHandler;
use XF\Db\DuplicateKeyException;
use XF\Entity\BookmarkItem;
use XF\Entity\BookmarkLabel;
use XF\Entity\User;
use XF\Finder\BookmarkItemFinder;
use XF\Finder\BookmarkLabelFinder;
use XF\Finder\BookmarkLabelUseFinder;
use XF\Mvc\Entity\AbstractCollection;
use XF\Mvc\Entity\Finder;
use XF\Mvc\Entity\Repository;
use XF\Util\Arr;
use XF\Util\Str;

use function intval, is_array, strlen;

class BookmarkRepository extends Repository
{
	/**
	 * @param $userId
	 *
	 * @return BookmarkItemFinder
	 */
	public function findBookmarksForUser($userId)
	{
		return $this->finder(BookmarkItemFinder::class)
			->where('user_id', $userId)
			->setDefaultOrder('bookmark_date', 'DESC');
	}

	/**
	 * @param $userId
	 * @param $labels
	 *
	 * @return BookmarkLabelUseFinder
	 */
	public function findBookmarksForUserByLabel($userId, $label)
	{
		return $this->finder(BookmarkLabelUseFinder::class)
			->with('Label', true)
			->with('Bookmark', true)
			->where('Label.user_id', $userId)
			->where('Label.label', $label)
			->setDefaultOrder('Bookmark.bookmark_date', 'DESC')
			->pluckFrom('Bookmark');
	}

	/**
	 * @param $userId
	 *
	 * @return BookmarkLabelFinder
	 */
	public function findLabelsForUser($userId)
	{
		return $this->finder(BookmarkLabelFinder::class)
			->where('user_id', $userId)
			->where('use_count', '>', 0)
			->setDefaultOrder('use_count', 'DESC');
	}

	/**
	 * @param AbstractCollection<BookmarkItem> $bookmarks
	 */
	public function addContentToBookmarks($bookmarks)
	{
		$contentMap = [];
		foreach ($bookmarks AS $key => $bookmark)
		{
			$contentType = $bookmark->content_type;
			if (!isset($contentMap[$contentType]))
			{
				$contentMap[$contentType] = [];
			}
			$contentMap[$contentType][$key] = $bookmark->content_id;
		}

		foreach ($contentMap AS $contentType => $contentIds)
		{
			$handler = $this->getBookmarkHandler($contentType);
			if (!$handler)
			{
				continue;
			}
			$data = $handler->getContent($contentIds);
			foreach ($contentIds AS $bookmarkId => $contentId)
			{
				$content = $data[$contentId] ?? null;
				$bookmarks[$bookmarkId]->setContent($content);
			}
		}
	}

	public function fastDeleteBookmarksForContent($contentType, $contentId)
	{
		$finder = $this->finder(BookmarkItemFinder::class)
			->where([
				'content_type' => $contentType,
				'content_id' => $contentId,
			]);
		$this->deleteBookmarksInternal($finder);
	}

	/**
	 * @param BookmarkItemFinder $matches
	 *
	 * @return void
	 */
	protected function deleteBookmarksInternal(Finder $matches)
	{
		$delete = $matches->fetchColumns('bookmark_id');

		$db = $this->db();
		$db->beginTransaction();

		if ($delete)
		{
			$db->delete('xf_bookmark_item', 'bookmark_id IN (' . $db->quote($delete) . ')');

			$labelIds = $db->fetchAllColumn('
				SELECT label_id
				FROM xf_bookmark_label_use
				WHERE bookmark_id IN(' . $db->quote($delete) . ')
			');

			if ($labelIds)
			{
				$db->delete('xf_bookmark_label_use', 'bookmark_id IN(' . $db->quote($delete) . ')');
				$this->recalculateLabelUsageCache($labelIds);
			}
		}

		$db->commit();
	}

	/**
	 * @param $type
	 * @param bool $throw
	 *
	 * @return AbstractHandler|null
	 * @throws \Exception
	 */
	public function getBookmarkHandler($type, $throw = false)
	{
		$handlerClass = \XF::app()->getContentTypeFieldValue($type, 'bookmark_handler_class');
		if (!$handlerClass)
		{
			if ($throw)
			{
				throw new \InvalidArgumentException("No Bookmark handler for '$type'");
			}
			return null;
		}

		if (!class_exists($handlerClass))
		{
			if ($throw)
			{
				throw new \InvalidArgumentException("Bookmark handler for '$type' does not exist: $handlerClass");
			}
			return null;
		}

		$handlerClass = \XF::extendClass($handlerClass);
		return new $handlerClass($type);
	}

	public function splitLabels($tagList)
	{
		return Arr::stringToArray($tagList, '/\s*,\s*/');
	}

	public function generateLabelUrlVersion($label, ?User $user = null)
	{
		if ($user === null)
		{
			$user = \XF::visitor();
		}

		$urlVersion = preg_replace('/[^a-zA-Z0-9_ -]/', '', Str::transliterate($label));
		$urlVersion = preg_replace('/[ -]+/', '-', $urlVersion);

		$db = $this->db();

		if (!strlen($urlVersion))
		{
			$urlVersion = 1 + intval($db->fetchOne("
				SELECT MAX(label_id)
				FROM xf_bookmark_label
				WHERE user_id = ?
			", $user->user_id));
		}
		else
		{
			$existing = $db->fetchRow("
				SELECT *
				FROM xf_bookmark_label
				WHERE (label_url = ?
					OR (label_url LIKE ? AND label_url REGEXP ?))
				AND user_id = ?
				ORDER BY label_id DESC
				LIMIT 1
			", [$urlVersion, "$urlVersion-%", "^{$urlVersion}-[0-9]+\$", $user->user_id]);
			if ($existing)
			{
				$counter = 1;
				if ($existing['label_url'] != $urlVersion && preg_match('/-(\d+)$/', $existing['label_url'], $match))
				{
					$counter = $match[1];
				}

				$testExists = true;
				while ($testExists)
				{
					$counter++;
					$testExists = $db->fetchOne("
						SELECT label_id
						FROM xf_bookmark_label
						WHERE label_url = ?
						AND user_id = ?
					", ["$urlVersion-$counter", $user->user_id]);
				}

				$urlVersion .= "-$counter";
			}
		}

		return $urlVersion;
	}

	public function getLabelsForUser(array $labelNames, User $user, &$notFound = [])
	{
		$notFound = [];

		$labels = $this->finder(BookmarkLabelFinder::class)
			->where('label', array_values($labelNames))
			->where('user_id', $user->user_id)
			->fetch();

		return $this->getNamedLabelsInList($labelNames, $labels->toArray(), $notFound);
	}

	public function getNamedLabelsInList(array $named, array $list, &$notFound = [])
	{
		$found = [];
		$notFound = [];

		$duplicateTest = [];

		foreach ($named AS $labelName)
		{
			$testLabelName = Str::normalize(Str::strtolower($labelName));
			if (isset($duplicateTest[$testLabelName]))
			{
				continue;
			}

			$duplicateTest[$testLabelName] = true;

			$foundKey = null;

			foreach ($list AS $key => $label)
			{
				if ($testLabelName == Str::normalize(Str::strtolower($label->label)))
				{
					$foundKey = $key;
					break;
				}
			}

			if ($foundKey === null)
			{
				$notFound[$testLabelName] = $labelName;
			}
			else
			{
				$found[$foundKey] = $list[$foundKey];
			}
		}

		$notFound = array_values($notFound); // prevent the same label potentially being not found multiple times

		return $found;
	}

	public function getLabelAutoCompleteResults($search, ?User $user = null, $maxResults = 10)
	{
		if ($user === null)
		{
			$user = \XF::visitor();
		}

		$finder = $this->finder(BookmarkLabelFinder::class);
		$labels = $finder
			->where('label', 'like', $finder->escapeLike($search, '?%'))
			->where('user_id', $user->user_id)
			->where('use_count', '>', 0)
			->order('label')
			->fetch($maxResults);

		if ($labels->count() < $maxResults)
		{
			$finder = $this->finder(BookmarkLabelFinder::class);
			$extraTags = $finder
				->where('label', 'like', $finder->escapeLike($search, '%?%'))
				->where('label', 'not like', $finder->escapeLike($search, '?%'))
				->where('user_id', $user->user_id)
				->where('use_count', '>', 0)
				->order('label')
				->fetch($maxResults - $labels->count());

			$labels = $labels->merge($extraTags);
		}

		return $labels;
	}

	public function createLabelForUser($labelName, User $user)
	{
		/** @var BookmarkLabel $label */
		$label = $this->em->create(BookmarkLabel::class);
		$label->label = $labelName;
		$label->user_id = $user->user_id;
		$label->preSave();

		if ($label->hasErrors())
		{
			return $this->getLabelForUser($user, $labelName);
		}

		try
		{
			$label->save();
		}
		catch (DuplicateKeyException $e)
		{
			$label = $this->getLabelForUser($user, $labelName);
		}

		return $label;
	}

	protected function getLabelForUser(User $user, string $labelName): ?BookmarkLabel
	{
		return $this->finder(BookmarkLabelFinder::class)
			->where('label', $labelName)
			->where('user_id', $user->user_id)
			->fetchOne();
	}

	public function modifyBookmarkLabelUses(BookmarkItem $bookmark, array $addIds, array $removeIds)
	{
		$db = $this->db();
		$db->beginTransaction();

		if ($removeIds)
		{
			$this->removeLabelUsesFromBookmark($removeIds, $bookmark->bookmark_id);
		}

		if ($addIds)
		{
			$this->addLabelIdsToBookmark($addIds, $bookmark->bookmark_id);
		}

		$cache = $this->getLabelUseCache($bookmark->bookmark_id);
		$bookmark->labels = $cache;
		$bookmark->save();

		$db->commit();

		return $cache;
	}

	protected function removeLabelUsesFromBookmark(array $labelIds, $bookmarkId)
	{
		if ($labelIds)
		{
			$db = $this->db();
			$db->query("
				DELETE FROM xf_bookmark_label_use
				WHERE label_id IN (" . $db->quote($labelIds) . ")
					AND bookmark_id = ?
			", $bookmarkId);
			$this->recalculateLabelUsageCache($labelIds);
		}
	}

	public function recalculateLabelUsageCache($labelIds)
	{
		if (!$labelIds)
		{
			return;
		}

		if (!is_array($labelIds))
		{
			$labelIds = [$labelIds];
		}

		$db = $this->db();

		$labels = $db->fetchAllColumn("
			SELECT label_id
			FROM xf_bookmark_label
			WHERE label_id IN (" . $db->quote($labelIds) . ")
		");
		$results = $db->fetchAllKeyed("
			SELECT label_id,
				COUNT(*) AS use_count,
				use_date AS last_use_date
			FROM xf_bookmark_label_use
			WHERE label_id IN (" . $db->quote($labelIds) . ")
			GROUP BY label_id
		", 'label_id');

		$db->beginTransaction();

		foreach ($labels AS $labelId)
		{
			$delete = false;

			if (isset($results[$labelId]))
			{
				$result = $results[$labelId];
				if (!$result['use_count'])
				{
					// this shouldn't actually happen since there shouldn't be a row
					$delete = true;
				}
				else
				{
					$db->update('xf_bookmark_label', [
						'use_count' => $result['use_count'],
						'last_use_date' => $result['last_use_date'],
					], 'label_id = ?', $labelId);
				}
			}
			else
			{
				$delete = true;
			}

			if ($delete)
			{
				$db->delete('xf_bookmark_label', 'label_id = ?', $labelId);
			}
		}

		$db->commit();
	}

	protected function addLabelIdsToBookmark(array $labelIds, $bookmarkId)
	{
		$db = $this->db();

		$insertedIds = [];

		foreach ($labelIds AS $addId)
		{
			$inserted = $db->insert('xf_bookmark_label_use', [
				'bookmark_id' => $bookmarkId,
				'label_id' => $addId,
				'use_date' => \XF::$time,
			], false, false, 'IGNORE');
			$contentLabelId = $db->lastInsertId();

			$db->query("
				UPDATE xf_bookmark_label
				SET use_count = use_count + 1,
					last_use_date = ?
				WHERE label_id = ?
			", [\XF::$time, $addId]);
			if ($inserted)
			{
				$insertedIds[$contentLabelId] = $addId;
			}
		}

		return $insertedIds;
	}

	public function rebuildBookmarkLabelCache($bookmarkId)
	{
		$bookmark = $this->em->find(BookmarkItem::class, $bookmarkId);
		if (!$bookmark)
		{
			return false;
		}

		$cache = $this->getLabelUseCache($bookmark->bookmark_id);

		$bookmark->labels = $cache;
		$bookmark->save();

		return true;
	}

	public function getLabelUseCache($bookmarkId)
	{
		$labels = $this->db()->fetchAll("
			SELECT l.*
			FROM xf_bookmark_label_use AS lu
			INNER JOIN xf_bookmark_label AS l ON (lu.label_id = l.label_id)
			WHERE lu.bookmark_id = ?
			ORDER BY l.label
		", $bookmarkId);
		$cache = [];
		foreach ($labels AS $label)
		{
			$cache[$label['label_id']] = [
				'label' => $label['label'],
				'label_url' => $label['label_url'],
			];
		}

		return $cache;
	}
}
