<?php

namespace XF\Service\Thread;

use XF\App;
use XF\Entity\Thread;
use XF\Job\SearchIndex;
use XF\Mvc\Entity\AbstractCollection;
use XF\Repository\ActivityLogRepository;
use XF\Repository\ContentVoteRepository;
use XF\Repository\ReactionRepository;
use XF\Repository\TagRepository;
use XF\Repository\ThreadRedirectRepository;
use XF\Repository\ThreadRepository;
use XF\Service\AbstractService;
use XF\Service\ModerationAlertSendableTrait;

use function intval, is_array;

class MergerService extends AbstractService
{
	use ModerationAlertSendableTrait;

	/**
	 * @var Thread
	 */
	protected $target;

	protected $alert = false;
	protected $alertReason = '';

	protected $redirect = false;
	protected $redirectLength = 0;

	protected $log = true;

	protected $sourceThreads = [];
	protected $sourcePosts = [];

	public function __construct(App $app, Thread $target)
	{
		parent::__construct($app);

		$this->target = $target;
	}

	public function getTarget()
	{
		return $this->target;
	}

	public function setSendAlert($alert, $reason = null)
	{
		$this->alert = (bool) $alert;
		if ($reason !== null)
		{
			$this->alertReason = $reason;
		}
	}

	public function setRedirect($redirect, $length = null)
	{
		$this->redirect = (bool) $redirect;
		if ($length !== null)
		{
			$this->redirectLength = intval($length);
		}
	}

	public function setLog($log)
	{
		$this->log = (bool) $log;
	}

	public function merge($sourceThreadsRaw)
	{
		if ($sourceThreadsRaw instanceof AbstractCollection)
		{
			$sourceThreadsRaw = $sourceThreadsRaw->toArray();
		}
		else if ($sourceThreadsRaw instanceof Thread)
		{
			$sourceThreadsRaw = [$sourceThreadsRaw];
		}
		else if (!is_array($sourceThreadsRaw))
		{
			throw new \InvalidArgumentException('Threads must be provided as collection, array or entity');
		}

		if (!$sourceThreadsRaw)
		{
			return false;
		}

		if ($this->alert)
		{
			$contentIds = [$this->target->node_id];
			$permissionCombinationIds = [];
			foreach ($sourceThreadsRaw AS $sourceThread)
			{
				/** @var Thread $sourceThread */
				if (!$sourceThread->user_id || !$sourceThread->User)
				{
					continue;
				}

				$contentIds[] = $sourceThread->node_id;
				$permissionCombinationIds[] = $sourceThread->User->permission_combination_id;
			}

			static::cacheContentPermissions(
				'node',
				$contentIds,
				$permissionCombinationIds
			);

			foreach ($sourceThreadsRaw AS $sourceThread)
			{
				/** @var Thread $sourceThread */
				$this->wasVisibleForAlert[$sourceThread->thread_id] = $this->isContentVisibleToContentAuthor(
					$sourceThread,
					$sourceThread
				);
				$this->isVisibleForAlert[$sourceThread->thread_id] = $this->isContentVisibleToContentAuthor(
					$this->target,
					$sourceThread
				);
			}
		}

		$db = $this->db();

		/** @var Thread[] $sourceThreads */
		$sourceThreads = [];
		foreach ($sourceThreadsRaw AS $sourceThread)
		{
			$sourceThread->setOption('log_moderator', false);
			$sourceThreads[$sourceThread->thread_id] = $sourceThread;
		}

		$posts = $db->fetchAllKeyed("
			SELECT post_id, thread_id, user_id, message_state, reactions
			FROM xf_post
			WHERE thread_id IN (" . $db->quote(array_keys($sourceThreads)) . ")
		", 'post_id');

		$this->sourceThreads = $sourceThreads;
		$this->sourcePosts = $posts;

		$target = $this->target;
		$target->setOption('log_moderator', false);

		$db->beginTransaction();

		$this->moveDataToTarget();
		$this->updateTargetData();
		$this->updateActivityLog();
		$this->updateUserCounters();
		$this->updateThreadReadData();

		if ($this->alert)
		{
			$this->sendAlert();
		}

		if ($this->redirect)
		{
			$this->convertSourcesToRedirects();
			$this->cleanUpSourceRedirects();
		}
		else
		{
			foreach ($sourceThreads AS $sourceThread)
			{
				$sourceThread->delete();
			}
		}

		$this->finalActions();

		$db->commit();

		return true;
	}

	protected function moveDataToTarget()
	{
		$db = $this->db();
		$target = $this->target;

		$sourceThreads = $this->sourceThreads;
		$sourceThreadIds = array_keys($sourceThreads);
		$sourceIdsQuoted = $db->quote($sourceThreadIds);

		$db->update(
			'xf_post',
			['thread_id' => $target->thread_id],
			"thread_id IN ($sourceIdsQuoted)"
		);
		$db->update(
			'xf_thread_watch',
			['thread_id' => $target->thread_id],
			"thread_id IN ($sourceIdsQuoted)",
			[],
			'IGNORE'
		);
		$db->update(
			'xf_thread_reply_ban',
			['thread_id' => $target->thread_id],
			"thread_id IN ($sourceIdsQuoted)",
			[],
			'IGNORE'
		);
		$db->update(
			'xf_tag_content',
			['content_id' => $target->thread_id],
			"content_type = 'thread' AND content_id IN ($sourceIdsQuoted)",
			[],
			'IGNORE'
		);

		foreach ($this->sourceThreads AS $sourceThread)
		{
			/** @var Thread $sourceThread */
			$db->update(
				'xf_news_feed',
				[
					'content_type' => 'post',
					'content_id' => $sourceThread->first_post_id,
				],
				"content_type = 'thread' AND content_id = ?",
				$sourceThread->thread_id,
				'IGNORE'
			);
		}

		$this->repository(ContentVoteRepository::class)->moveVotesBetweenContent($target, $sourceThreads);
	}

	protected function updateTargetData()
	{
		$target = $this->target;
		$sourceThreads = $this->sourceThreads;

		foreach ($sourceThreads AS $sourceThread)
		{
			$target->view_count += $sourceThread->view_count;
		}

		$target->TypeHandler->onThreadMergeInto($target, $sourceThreads);

		$target->rebuildCounters();
		$target->save();

		/** @var ThreadRepository $threadRepo */
		$threadRepo = $this->repository(ThreadRepository::class);
		$threadRepo->rebuildThreadPostPositions($target->thread_id);
		$threadRepo->rebuildThreadUserPostCounters($target->thread_id);

		/** @var TagRepository $tagRepo */
		$tagRepo = $this->repository(TagRepository::class);
		$tagRepo->rebuildContentTagCache('thread', $target->thread_id);
	}

	protected function updateActivityLog(): void
	{
		$activityLogRepo = $this->repository(ActivityLogRepository::class);

		$activityLogRepo->mergeLogs(
			$this->target,
			array_keys($this->sourceThreads),
			['view_count']
		);

		$activityLogRepo->rebuildReplyMetrics($this->target);
		$activityLogRepo->rebuildVoteMetrics($this->target);

		foreach ($this->sourceThreads AS $sourceThread)
		{
			if ($sourceThread->post_date <= $this->target->post_date)
			{
				$activityLogRepo->rebuildReactionMetrics($this->target);
				break;
			}
		}
	}

	protected function updateUserCounters()
	{
		$target = $this->target;

		$targetMessagesCount = (
			$target->Forum && $target->Forum->count_messages
			&& $target->discussion_state == 'visible'
		);
		$targetReactionsCount = ($target->discussion_state == 'visible');

		$sourcesMessagesCount = [];
		$sourcesReactionsCount = [];
		foreach ($this->sourceThreads AS $id => $sourceThread)
		{
			$sourcesMessagesCount[$id] = (
				$sourceThread->Forum && $sourceThread->Forum->count_messages
				&& $sourceThread->discussion_state == 'visible'
			);
			$sourcesReactionsCount[$id] = ($sourceThread->discussion_state == 'visible');
		}

		$reactionsEnable = [];
		$reactionsDisable = [];
		$userMessageCountAdjust = [];

		foreach ($this->sourcePosts AS $id => $post)
		{
			if ($post['message_state'] != 'visible')
			{
				continue; // everything will stay the same in the new thread
			}

			$sourceMessagesCount = $sourcesMessagesCount[$post['thread_id']];
			$sourceReactionsCount = $sourcesReactionsCount[$post['thread_id']];

			if ($post['reactions'])
			{
				if ($sourceReactionsCount && !$targetReactionsCount)
				{
					$reactionsDisable[] = $id;
				}
				else if (!$sourceReactionsCount && $targetReactionsCount)
				{
					$reactionsEnable[] = $id;
				}
			}

			$userId = $post['user_id'];
			if ($userId)
			{
				if ($sourceMessagesCount && !$targetMessagesCount)
				{
					if (!isset($userMessageCountAdjust[$userId]))
					{
						$userMessageCountAdjust[$userId] = 0;
					}
					$userMessageCountAdjust[$userId]--;
				}
				else if (!$sourceMessagesCount && $targetMessagesCount)
				{
					if (!isset($userMessageCountAdjust[$userId]))
					{
						$userMessageCountAdjust[$userId] = 0;
					}
					$userMessageCountAdjust[$userId]++;
				}
			}
		}

		if ($reactionsDisable)
		{
			/** @var ReactionRepository $reactionRepo */
			$reactionRepo = $this->repository(ReactionRepository::class);
			$reactionRepo->fastUpdateReactionIsCounted('post', $reactionsDisable, false);
		}
		if ($reactionsEnable)
		{
			/** @var ReactionRepository $reactionRepo */
			$reactionRepo = $this->repository(ReactionRepository::class);
			$reactionRepo->fastUpdateReactionIsCounted('post', $reactionsEnable, true);
		}
		foreach ($userMessageCountAdjust AS $userId => $adjust)
		{
			if ($adjust)
			{
				$this->db()->query("
					UPDATE xf_user
					SET message_count = GREATEST(0, message_count + ?)
					WHERE user_id = ?
				", [$adjust, $userId]);
			}
		}
	}

	protected function updateThreadReadData()
	{
		$sourceThreadIds = $this->db()->quote(array_keys($this->sourceThreads));

		$this->db()->query("
			UPDATE xf_thread_read AS tr_dest,
			(
			    SELECT MIN(thread_read_date) AS min_thread_read_date, user_id
			    FROM xf_thread_read
				WHERE thread_id IN({$sourceThreadIds})
				GROUP BY user_id
			) AS tr_src
			SET tr_dest.thread_read_date = tr_src.min_thread_read_date
			WHERE tr_dest.user_id = tr_src.user_id
			AND tr_dest.thread_id = ?
		", $this->target->thread_id);
	}

	protected function sendAlert()
	{
		$target = $this->target;
		$actor = \XF::visitor();

		/** @var ThreadRepository $threadRepo */
		$threadRepo = $this->repository(ThreadRepository::class);

		$alertExtras = [
			'targetTitle' => $target->title,
			'targetLink' => $this->app->router('public')->buildLink('nopath:threads', $target),
		];

		foreach ($this->sourceThreads AS $sourceThread)
		{
			if ($sourceThread->discussion_state == 'visible'
				&& $sourceThread->user_id != $actor->user_id
				&& $sourceThread->discussion_type != 'redirect'
				&& (
					!empty($this->wasVisibleForAlert[$sourceThread->thread_id])
					|| !empty($this->isVisibleForAlert[$sourceThread->thread_id])
				)
			)
			{
				$threadRepo->sendModeratorActionAlert($sourceThread, 'merge', $this->alertReason, $alertExtras);
			}
		}
	}

	protected function convertSourcesToRedirects()
	{
		$target = $this->target;

		/** @var ThreadRedirectRepository $redirectRepo */
		$redirectRepo = $this->repository(ThreadRedirectRepository::class);

		foreach ($this->sourceThreads AS $sourceThread)
		{
			$sourceThread->discussion_type = 'redirect';
			$redirectRepo->createRedirectionRecordForThread($sourceThread, $target, $this->redirectLength, false);
			$sourceThread->save();
		}
	}

	protected function cleanUpSourceRedirects()
	{
		$db = $this->db();
		$sourceThreadIds = array_keys($this->sourceThreads);
		$sourceIdsQuoted = $db->quote($sourceThreadIds);

		$db->delete('xf_thread_watch', "thread_id IN ($sourceIdsQuoted)");
		$db->delete('xf_thread_view', "thread_id IN ($sourceIdsQuoted)");
		$db->delete('xf_thread_reply_ban', "thread_id IN ($sourceIdsQuoted)");
		$db->delete('xf_thread_user_post', "thread_id IN ($sourceIdsQuoted)");
		$db->delete('xf_poll', "content_type = 'thread' AND content_id IN ($sourceIdsQuoted)");

		$this->app->search()->delete('thread', $sourceThreadIds);
	}

	protected function finalActions()
	{
		$target = $this->target;
		$sourceThreads = $this->sourceThreads;
		$sourceThreadIds = array_keys($sourceThreads);
		$postIds = array_keys($this->sourcePosts);

		if ($postIds)
		{
			$this->app->jobManager()->enqueue(SearchIndex::class, [
				'content_type' => 'post',
				'content_ids' => $postIds,
			]);
		}

		if ($this->log)
		{
			$this->app->logger()->logModeratorAction(
				'thread',
				$target,
				'merge_target',
				['ids' => implode(', ', $sourceThreadIds)]
			);
		}
	}
}
