<?php

namespace XF\Repository;

use XF\Db\DuplicateKeyException;
use XF\Entity\ReactionContent;
use XF\Entity\User;
use XF\Finder\ReactionContentFinder;
use XF\Finder\ReactionFinder;
use XF\Mvc\Entity\Repository;
use XF\Reaction\AbstractHandler;

use function intval, is_array;

class ReactionRepository extends Repository
{
	/**
	 * @return ReactionFinder
	 */
	public function findReactionsForList($activeOnly = false)
	{
		$finder = $this->finder(ReactionFinder::class)
			->order('display_order');

		if ($activeOnly)
		{
			$finder->where('active', 1);
		}

		return $finder;
	}

	public function getReactionScores()
	{
		return [
			1 => \XF::phrase('reaction_score.positive'),
			0 => \XF::phrase('reaction_score.neutral'),
			-1 => \XF::phrase('reaction_score.negative'),
		];
	}

	/**
	 * @param string $contentType
	 * @param int $contentId
	 * @param int $userId
	 *
	 * @return ReactionContent|null
	 */
	public function getReactionByContentAndReactionUser($contentType, $contentId, $userId)
	{
		return $this->finder(ReactionContentFinder::class)->where([
			'content_type' => $contentType,
			'content_id' => $contentId,
			'reaction_user_id' => $userId,
		])->fetchOne();
	}

	/**
	 * @param string $contentType
	 * @param int $contentId
	 *
	 * @return ReactionContentFinder
	 */
	public function findContentReactions($contentType, $contentId, $reactionId = null)
	{
		$finder = $this->finder(ReactionContentFinder::class)
			->with([
				'Reaction',
				'ReactionUser',
				'ReactionUser.Profile',
				'ReactionUser.Option',
			], true)
			->where([
				'content_type' => $contentType,
				'content_id' => $contentId,
				'Reaction.active' => true,
			])
			->setDefaultOrder('reaction_date', 'DESC');

		if ($reactionId)
		{
			$finder->where('reaction_id', $reactionId);
		}

		return $finder;
	}

	/**
	 * @param $reactionUserId
	 *
	 * @return ReactionContentFinder
	 */
	public function findReactionsByReactionUserId($reactionUserId)
	{
		if ($reactionUserId instanceof User)
		{
			$reactionUserId = $reactionUserId->user_id;
		}

		return $this->finder(ReactionContentFinder::class)
			->where('reaction_user_id', $reactionUserId)
			->setDefaultOrder('reaction_date');
	}

	public function getContentTabSummary($contentType, $contentId)
	{
		$reactionHandler = $this->getReactionHandler($contentType, true);

		$entity = $reactionHandler->getContent($contentId);
		if (!$entity)
		{
			throw new \InvalidArgumentException("No entity found for '$contentType' with ID $contentId");
		}

		$countsField = $reactionHandler->getCountsFieldName();

		$counts = $entity->$countsField;
		$counts = [0 => array_sum($counts)] + $counts;
		return $counts;
	}

	public function reactToContent($reactionId, $contentType, $contentId, User $reactUser, $publish = true, $isLike = false)
	{
		$insert = false;

		$existingReaction = $this->getReactionByContentAndReactionUser($contentType, $contentId, $reactUser->user_id);
		if ($existingReaction && $existingReaction->reaction_id == $reactionId)
		{
			$existingReaction->setOption('is_like_only', $isLike);
			$existingReaction->delete();

			$reactionHandler = $this->getReactionHandler($contentType, false, $isLike);
			if ($reactionHandler)
			{
				$entity = $reactionHandler->getContent($contentId);
				if ($entity)
				{
					$this->repository(WebhookRepository::class)->queueWebhook(
						$contentType,
						$contentId,
						'unreact',
						$entity
					);
				}
			}

			return null;
		}
		else if ($existingReaction && $existingReaction->reaction_id != $reactionId)
		{
			$existingReaction->setOption('is_like_only', $isLike);
			$existingReaction->delete();
			$insert = true;
		}
		else if (!$existingReaction)
		{
			$insert = true;
		}

		if ($insert)
		{
			try
			{
				$reaction = $this->insertReaction(
					$reactionId,
					$contentType,
					$contentId,
					$reactUser,
					$publish,
					$isLike
				);
				return $reaction;
			}
			catch (DuplicateKeyException $e)
			{
				// race condition so we should just re-look up the reaction and return that
				return $this->getReactionByContentAndReactionUser($contentType, $contentId, $reactUser->user_id);
			}
		}
		else
		{
			return null;
		}
	}

	public function insertReaction($reactionId, $contentType, $contentId, User $reactUser, $publish = true, $isLike = false)
	{
		if (!$reactUser->user_id)
		{
			throw new \InvalidArgumentException("Guests cannot react to content");
		}

		$reactionHandler = $this->getReactionHandler($contentType, true, $isLike);

		$entity = $reactionHandler->getContent($contentId);
		if (!$entity)
		{
			throw new \InvalidArgumentException("No entity found for '$contentType' with ID $contentId");
		}

		/** @var ReactionContent $reaction */
		$reaction = $this->em->create(ReactionContent::class);
		$reaction->setOption('is_like_only', $isLike);
		$reaction->reaction_id = $reactionId;
		$reaction->content_type = $contentType;
		$reaction->content_id = $contentId;
		$reaction->reaction_user_id = $reactUser->user_id;
		$reaction->content_user_id = $reactionHandler->getContentUserId($entity);
		if ($isLike)
		{
			$reaction->is_counted = $reactionHandler->likesCounted($entity);
		}
		else
		{
			$reaction->is_counted = $reactionHandler->reactionsCounted($entity);
		}
		$reaction->save();

		if ($publish)
		{
			if ($reaction->Owner && $reaction->ReactionUser)
			{
				if ($isLike)
				{
					$reactionHandler->sendLikeAlert($reaction->Owner, $reaction->ReactionUser, $contentId, $entity);
				}
				else
				{
					$reactionHandler->sendReactionAlert($reaction->Owner, $reaction->ReactionUser, $contentId, $entity, $reactionId);
				}
			}
			if ($reaction->ReactionUser)
			{
				if ($isLike)
				{
					$reactionHandler->publishLikeNewsFeed($reaction->ReactionUser, $contentId, $entity);
				}
				else
				{
					$reactionHandler->publishReactionNewsFeed($reaction->ReactionUser, $contentId, $entity, $reactionId);
				}
			}
		}

		$this->repository(WebhookRepository::class)->queueWebhook(
			$contentType,
			$contentId,
			'react',
			$entity
		);

		return $reaction;
	}

	public function rebuildContentReactionCache($contentType, $contentId, $isLike = false, $throw = true)
	{
		$reactionHandler = $this->getReactionHandler($contentType, $throw, $isLike);
		if (!$reactionHandler)
		{
			if ($throw)
			{
				throw new \InvalidArgumentException("No reaction handler found for '$contentType'");
			}
			return false;
		}

		$entity = $reactionHandler->getContent($contentId);
		if (!$entity)
		{
			if ($throw)
			{
				throw new \InvalidArgumentException("No entity found for '$contentType' with ID $contentId");
			}
			return false;
		}

		$counts = $this->db()->fetchPairs("
			SELECT reacted.reaction_id, COUNT(*) AS counts
			FROM xf_reaction_content AS reacted
			INNER JOIN xf_reaction AS reaction ON (reacted.reaction_id = reaction.reaction_id)
			WHERE content_type = ? AND content_id = ? AND reaction.active = 1
			GROUP BY reaction_id
			ORDER BY counts DESC
		", [$contentType, $contentId]);

		if ($counts)
		{
			$latest = $this->db()->fetchAll("
				SELECT user.user_id, user.username, reacted.reaction_id
				FROM xf_reaction_content AS reacted
				INNER JOIN xf_user AS user ON (reacted.reaction_user_id = user.user_id)
				INNER JOIN xf_reaction AS reaction ON (reacted.reaction_id = reaction.reaction_id)
				WHERE reacted.content_type = ? AND reacted.content_id = ? AND reaction.active = 1
				ORDER BY reacted.reaction_date DESC
				LIMIT 5
			", [$contentType, $contentId]);
		}
		else
		{
			$latest = [];
		}

		if ($isLike)
		{
			$reactionHandler->updateContentLikes($entity, $counts, $latest);
		}
		else
		{
			$reactionHandler->updateContentReactions($entity, $counts, $latest);
		}

		return true;
	}

	/**
	 * @return array<string, AbstractHandler>
	 */
	public function getReactionHandlers($isLike = false)
	{
		$handlers = [];

		$field = $isLike ? 'like_handler_class' : 'reaction_handler_class';
		foreach (\XF::app()->getContentTypeField($field) AS $contentType => $handlerClass)
		{
			if (class_exists($handlerClass))
			{
				$handlerClass = \XF::extendClass($handlerClass);
				$handlers[$contentType] = new $handlerClass($contentType);
			}
		}

		return $handlers;
	}

	/**
	 * @param string $type
	 * @param bool $throw
	 *
	 * @return AbstractHandler|\XF\Like\AbstractHandler|null
	 */
	public function getReactionHandler($type, $throw = false, $isLike = false)
	{
		$field = $isLike ? 'like_handler_class' : 'reaction_handler_class';
		$handlerClass = \XF::app()->getContentTypeFieldValue($type, $field);
		if (!$handlerClass)
		{
			if ($throw)
			{
				throw new \InvalidArgumentException("No reaction handler for '$type'");
			}
			return null;
		}

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

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

	/**
	 * @param ReactionContent[] $reactions
	 */
	public function addContentToReactions($reactions, $isLike = false)
	{
		$contentMap = [];
		foreach ($reactions AS $key => $reaction)
		{
			$contentType = $reaction->content_type;
			if (!isset($contentMap[$contentType]))
			{
				$contentMap[$contentType] = [];
			}
			$contentMap[$contentType][$key] = $reaction->content_id;
		}

		foreach ($contentMap AS $contentType => $contentIds)
		{
			$handler = $this->getReactionHandler($contentType, false, $isLike);
			if (!$handler)
			{
				continue;
			}

			$data = $handler->getContent($contentIds);

			foreach ($contentIds AS $reactionContentId => $contentId)
			{
				$content = $data[$contentId] ?? null;
				$reactions[$reactionContentId]->setContent($content);
			}
		}
	}

	public function recalculateReactionIsCounted($contentType, $contentIds, $updateReactionScore = true, $isLike = false)
	{
		$reactionHandler = $this->getReactionHandler($contentType, true, $isLike);

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

		$entities = $reactionHandler->getContent($contentIds);
		$enableIds = [];
		$disableIds = [];

		foreach ($entities AS $id => $entity)
		{
			if ($isLike)
			{
				$isCounted = $reactionHandler->likesCounted($entity);
			}
			else
			{
				$isCounted = $reactionHandler->reactionsCounted($entity);
			}

			if ($isCounted)
			{
				$enableIds[] = $id;
			}
			else
			{
				$disableIds[] = $id;
			}
		}

		if ($enableIds)
		{
			$this->fastUpdateReactionIsCounted($contentType, $enableIds, true, $updateReactionScore);
		}
		if ($disableIds)
		{
			$this->fastUpdateReactionIsCounted($contentType, $disableIds, false, $updateReactionScore);
		}
	}

	public function fastUpdateReactionIsCounted($contentType, $contentIds, $newValue, $updateReactionScore = true)
	{
		if (!is_array($contentIds))
		{
			$contentIds = [$contentIds];
		}
		if (!$contentIds)
		{
			return;
		}

		$newDbValue = $newValue ? 1 : 0;
		$oldDbValue = $newValue ? 0 : 1;

		$db = $this->db();
		if ($updateReactionScore)
		{
			$updates = $db->fetchAll("
				SELECT reaction_user_id, content_user_id, content_id
				FROM xf_reaction_content
				WHERE content_type = ?
					AND content_id IN (" . $db->quote($contentIds) . ")
					AND is_counted = ?
			", [$contentType, $oldDbValue]);
			if ($updates)
			{
				$db->beginTransaction();

				$db->update(
					'xf_reaction_content',
					['is_counted' => $newDbValue],
					'content_type = ?
						AND content_id IN (' . $db->quote($contentIds) . ')
						AND is_counted = ?',
					[$contentType, $oldDbValue]
				);

				$tally = [];
				foreach ($updates AS $update)
				{
					if (!$update['content_user_id'])
					{
						continue;
					}

					$existingReaction = $this->getReactionByContentAndReactionUser($contentType, $update['content_id'], $update['reaction_user_id']);
					if (!empty($existingReaction->Reaction->reaction_score))
					{
						$tally[$update['content_user_id']] = isset($tally[$update['content_user_id']]) ? $tally[$update['content_user_id']] + $existingReaction->Reaction->reaction_score : $existingReaction->Reaction->reaction_score;
					}
				}

				$operator = $newDbValue ? '+' : '-';
				foreach ($tally AS $userId => $totalChange)
				{
					$db->query("
						UPDATE xf_user
						SET reaction_score = reaction_score {$operator} ?
						WHERE user_id = ?
					", [$totalChange, $userId]);
				}

				$db->commit();
			}
		}
		else
		{
			$db->update(
				'xf_reaction_content',
				['is_counted' => $newDbValue],
				'content_type = ?
					AND content_id IN (' . $db->quote($contentIds) . ')
					AND is_counted = ?',
				[$contentType, $oldDbValue]
			);
		}
	}

	public function fastDeleteReactions($contentType, $contentIds, $updateReactionCount = true)
	{
		if (!is_array($contentIds))
		{
			$contentIds = [$contentIds];
		}
		if (!$contentIds)
		{
			return;
		}

		$db = $this->db();

		if ($updateReactionCount)
		{
			$updates = $db->fetchAll("
				SELECT reaction_user_id, content_user_id, content_id
				FROM xf_reaction_content
				WHERE content_type = ?
					AND content_id IN (" . $db->quote($contentIds) . ")
					AND is_counted = 1
			", $contentType);
		}
		else
		{
			$updates = [];
		}

		$db->beginTransaction();
		if ($updates)
		{
			$tally = [];
			foreach ($updates AS $update)
			{
				if (!$update['content_user_id'])
				{
					continue;
				}

				$existingReaction = $this->getReactionByContentAndReactionUser($contentType, $update['content_id'], $update['reaction_user_id']);
				if (!empty($existingReaction->Reaction->reaction_score))
				{
					$tally[$update['content_user_id']] = isset($tally[$update['content_user_id']]) ? $tally[$update['content_user_id']] + $existingReaction->Reaction->reaction_score : $existingReaction->Reaction->reaction_score;
				}
			}

			foreach ($tally AS $userId => $totalChange)
			{
				$db->query("
					UPDATE xf_user
					SET reaction_score = reaction_score - ?
					WHERE user_id = ?
				", [$totalChange, $userId]);
			}
		}

		$db->delete(
			'xf_reaction_content',
			'content_type = ? AND content_id IN (' . $db->quote($contentIds) . ')',
			$contentType
		);

		$db->commit();
	}

	public function getUserReactionScore($userId)
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		return $this->getUserReactionScoreSince($userId);
	}

	public function getUserReactionScoreSince($userId, $date = 0)
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		return intval($this->db()->fetchOne("
			SELECT SUM(reaction.reaction_score)
			FROM xf_reaction_content AS content
			INNER JOIN xf_reaction AS reaction ON
				(content.reaction_id = reaction.reaction_id)
			WHERE content.content_user_id = ?
				AND content.is_counted = 1
				AND content.reaction_date > ?
		", [$userId, $date]));
	}

	/**
	 * @param User|int $userId
	 */
	public function getReactionScoreGivenByUserSince($userId, int $date = 0): int
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		return $this->db()->fetchOne(
			'SELECT SUM(reaction.reaction_score)
				FROM xf_reaction_content AS reaction_content
				INNER JOIN xf_reaction AS reaction ON
					(reaction.reaction_id = reaction_content.reaction_id)
				WHERE reaction_content.reaction_user_id = ?
					AND reaction_content.is_counted = 1
					AND reaction_content.reaction_date > ?',
			[$userId, $date]
		);
	}

	/**
	 * @param $userId
	 *
	 * @return ReactionContentFinder
	 */
	public function findUserReactions($userId)
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		$finder = $this->finder(ReactionContentFinder::class)
			->with('ReactionUser')
			->where('content_user_id', $userId)
			->where('is_counted', 1)
			->setDefaultOrder('reaction_date', 'DESC');

		return $finder;
	}

	public function getUserReactionsTabSummary($userId)
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		$activeReactionIds = [];
		foreach ($this->app()->get('reactions') AS $reactionId => $reaction)
		{
			if (!empty($reaction['active']))
			{
				$activeReactionIds[] = $reactionId;
			}
		}

		if (!$activeReactionIds)
		{
			return [];
		}

		$db = $this->db();

		$result = $db->fetchPairs('
			SELECT content.reaction_id, COUNT(*)
			FROM xf_reaction_content AS content
			WHERE content.content_user_id = ?
				AND content.is_counted = 1
				AND content.reaction_id IN (' . $db->quote($activeReactionIds) . ')
			GROUP BY content.reaction_id
		', $userId);

		$sorted = [];
		foreach ($activeReactionIds AS $reactionId)
		{
			if (isset($result[$reactionId]))
			{
				$sorted[$reactionId] = $result[$reactionId];
			}
		}

		return $sorted;
	}

	public function getReactionCacheData()
	{
		$reactions = $this->finder(ReactionFinder::class)
			->order(['display_order', 'reaction_id'])
			->fetch();

		$cache = [];

		foreach ($reactions AS $reactionId => $reaction)
		{
			$reaction = $reaction->toArray();

			$cache[$reactionId] = $reaction;

			if (!$reaction['sprite_mode'] || !$reaction['sprite_params'])
			{
				unset($cache[$reactionId]['sprite_params']);
			}

			unset($cache[$reactionId]['sprite_mode']);
		}

		return $cache;
	}

	public function rebuildReactionCache()
	{
		$cache = $this->getReactionCacheData();
		\XF::registry()->set('reactions', $cache);
		return $cache;
	}

	public function getReactionSpriteCacheData()
	{
		$reactions = $this->finder(ReactionFinder::class)
			->order(['display_order', 'reaction_id'])
			->fetch();

		$cache = [];
		$defaultReactionHeight = 32;

		foreach ($reactions AS $reactionId => $reaction)
		{
			if ($reaction->sprite_mode && !empty($reaction->sprite_params))
			{
				$w = (int) $reaction->sprite_params['w'];
				$h = (int) $reaction->sprite_params['h'];
				$x = (int) $reaction->sprite_params['x'];
				$y = (int) $reaction->sprite_params['y'];
				$imageUrlHtml = htmlspecialchars($reaction->image_url);

				// Out of the box reactions display at 32x32 for the max size. We then generally assume
				// small and medium are 16x16 and 21x21, so we need to ensure that we always scale other
				// reaction sizes to that.
				$adjustScalingFactor = $defaultReactionHeight / $h;

				$cache[$reactionId]['sprite_css'] = sprintf(
					'width: %1$dpx; height: %2$dpx; background: url(\'%3$s\') no-repeat %4$dpx %5$dpx;',
					$w,
					$h,
					$imageUrlHtml,
					$x,
					$y
				);

				$cache[$reactionId]['small_sprite_css'] = sprintf(
					'width: %1$dpx; height: %2$dpx; background: url(\'%3$s\') no-repeat %4$dpx %5$dpx;',
					($w / 2) * $adjustScalingFactor,
					($h / 2) * $adjustScalingFactor,
					$imageUrlHtml,
					($x / 2) * $adjustScalingFactor,
					($y / 2) * $adjustScalingFactor
				);

				$cache[$reactionId]['medium_sprite_css'] = sprintf(
					'width: %1$dpx; height: %2$dpx; background: url(\'%3$s\') no-repeat %4$dpx %5$dpx;',
					($w * .65625) * $adjustScalingFactor,
					($h * .65625) * $adjustScalingFactor,
					$imageUrlHtml,
					($x * .65625) * $adjustScalingFactor,
					($y * .65625) * $adjustScalingFactor
				);

				if (!empty($reaction->sprite_params['bs']))
				{
					$bs = ' background-size: ' . htmlspecialchars($reaction->sprite_params['bs']);

					$cache[$reactionId]['sprite_css'] .= $bs;
					$cache[$reactionId]['small_sprite_css'] .= $bs;
					$cache[$reactionId]['medium_sprite_css'] .= $bs;
				}
			}
		}

		return $cache;
	}

	public function rebuildReactionSpriteCache()
	{
		$cache = $this->getReactionSpriteCacheData();
		\XF::registry()->set('reactionSprites', $cache);
		$this->repository(StyleRepository::class)->updateAllStylesLastModifiedDateLater();
		return $cache;
	}
}
