<?php

namespace XF\Service\Post;

use XF\App;
use XF\Entity\Post;
use XF\Entity\Thread;
use XF\Mvc\Entity\AbstractCollection;
use XF\Repository\ActivityLogRepository;
use XF\Repository\PostRepository;
use XF\Repository\ThreadRepository;
use XF\Service\AbstractService;
use XF\Service\ModerationAlertSendableTrait;
use XF\Util\Arr;

use function intval, is_array;

class CopierService extends AbstractService
{
	use ModerationAlertSendableTrait;

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

	protected $existingTarget = false;

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

	protected $prefixId = null;

	protected $log = true;

	/**
	 * @var Thread[]
	 */
	protected $sourceThreads = [];

	/**
	 * @var Post[]
	 */
	protected $sourcePosts = [];

	public function __construct(App $app, Thread $target)
	{
		parent::__construct($app);
		$this->target = $target;
	}

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

	public function setExistingTarget($existing)
	{
		$this->existingTarget = (bool) $existing;
	}

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

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

	public function setPrefix($prefixId)
	{
		$this->prefixId = ($prefixId === null ? $prefixId : intval($prefixId));
	}

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

		if (!$sourcePostsRaw)
		{
			return false;
		}

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

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

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

			foreach ($sourcePostsRaw AS $sourcePost)
			{
				/** @var Post $sourcePost */
				$this->wasVisibleForAlert[$sourcePost->post_id] = $this->isContentVisibleToContentAuthor(
					$sourcePost,
					$sourcePost
				);
				$this->isVisibleForAlert[$sourcePost->post_id] = $this->isContentVisibleToContentAuthor(
					$this->target,
					$sourcePost
				);
			}
		}

		$db = $this->db();

		/** @var Post[] $sourcePosts */
		/** @var Thread[] $sourceThreads */
		$sourcePosts = [];
		$sourceThreads = [];

		foreach ($sourcePostsRaw AS $sourcePost)
		{
			$sourcePost->setOption('log_moderator', false);
			$sourcePosts[$sourcePost->post_id] = $sourcePost;

			/** @var Thread $sourceThread */
			$sourceThread = $sourcePost->Thread;
			if (!isset($sourceThreads[$sourceThread->thread_id]))
			{
				$sourceThread->setOption('log_moderator', false);
				$sourceThreads[$sourceThread->thread_id] = $sourceThread;
			}
		}

		$sourcePosts = Arr::columnSort($sourcePosts, 'post_date');

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

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

		if (!$target->thread_id)
		{
			$firstPost = reset($sourcePosts);

			$target->user_id = $firstPost->user_id;
			$target->username = $firstPost->username;
			$target->post_date = $firstPost->post_date;
		}

		$db->beginTransaction();

		$target->save();

		$this->copyDataToTarget();
		$this->updateTargetData();
		$this->updateActivityLog();

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

		$this->finalActions();

		$db->commit();

		return true;
	}

	protected function copyDataToTarget()
	{
		$resetValues = $this->getResetPostData();

		$position = 0;
		$firstPost = $this->existingTarget ? $this->target->FirstPost : null;

		// posts are sorted in date order
		foreach ($this->sourcePosts AS $sourcePost)
		{
			/** @var Post $newPost */
			$newPost = $this->em()->create(Post::class);

			$values = $sourcePost->toArray(false);
			foreach ($resetValues AS $key)
			{
				unset($values[$key]);
			}

			$newPost->thread_id = $this->target->thread_id;
			$newPost->bulkSet($values);

			$newPost->position = $position;
			if (!$firstPost)
			{
				// first post is always visible, set $firstPost later
				$this->target->discussion_state = $newPost->message_state;
				$newPost->message_state = 'visible';
			}

			$newPost->save();

			if (!$firstPost)
			{
				$firstPost = $newPost;

				$this->target->fastUpdate('first_post_id', $newPost->post_id);
			}

			$embedMetadata = $sourcePost->embed_metadata;
			$newPost->embed_metadata = $this->updateEmbeds($sourcePost, $newPost, $embedMetadata ?: []);
			$newPost->saveIfChanged();

			if ($newPost->message_state == 'visible')
			{
				$position++;
			}
		}
	}

	protected function getResetPostData()
	{
		return  [
			'post_id',
			'thread_id',
			'position',
			'reaction_score',
			'reactions',
			'reaction_users',
			'warning_id',
			'warning_message',
			'last_edit_date',
			'last_edit_user_id',
			'edit_count',
			'vote_score',
			'vote_count',
		];
	}

	protected function updateEmbeds(Post $sourcePost, Post $newPost, array $embedMetadata)
	{
		$attachEmbed = $embedMetadata['attachments'] ?? [];

		foreach ($sourcePost->Attachments AS $sourceAttachment)
		{
			$newAttachment = $sourceAttachment->createDuplicate();
			$newAttachment->content_type = 'post';
			$newAttachment->content_id = $newPost->post_id;
			$newAttachment->save();

			$newPost->message = preg_replace(
				'#(\[attach[^\]]*\])' . $sourceAttachment->attachment_id . '(\[/attach\])#i',
				'${1}' . $newAttachment->attachment_id . '${2}',
				$newPost->message
			);

			if (isset($attachEmbed[$sourceAttachment->attachment_id]))
			{
				unset($attachEmbed[$sourceAttachment->attachment_id]);
				$attachEmbed[$newAttachment->attachment_id] = $newAttachment->attachment_id;
			}
		}

		if ($attachEmbed)
		{
			$embedMetadata['attachments'] = $attachEmbed;
		}
		else
		{
			unset($embedMetadata['attachments']);
		}

		return $embedMetadata;
	}

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

		if ($this->prefixId !== null)
		{
			$target->prefix_id = $this->prefixId;
		}
		$target->rebuildCounters();
		$target->save();

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

		$threadRepo = $this->repository(ThreadRepository::class);
		$threadRepo->rebuildThreadPostPositions($target->thread_id);
		$threadRepo->rebuildThreadUserPostCounters($target->thread_id);
	}

	protected function updateActivityLog(): void
	{
		if (!$this->existingTarget)
		{
			return;
		}

		foreach ($this->sourcePosts AS $sourcePost)
		{
			if ($sourcePost->post_date <= $this->target->post_date)
			{
				$activityLogRepo = $this->repository(ActivityLogRepository::class);
				$activityLogRepo->rebuildReactionMetrics($this->target);
				break;
			}
		}
	}

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

		$postRepo = $this->repository(PostRepository::class);

		foreach ($this->sourcePosts AS $sourcePost)
		{
			if ($sourcePost->Thread->discussion_state == 'visible'
				&& $sourcePost->message_state == 'visible'
				&& $sourcePost->user_id != \XF::visitor()->user_id
				&& (
					!empty($this->wasVisibleForAlert[$sourcePost->post_id])
					|| !empty($this->isVisibleForAlert[$sourcePost->post_id])
				)
			)
			{
				$alertExtras = [
					'targetTitle' => $target->title,
					'targetLink' => $this->app->router('public')->buildLink('nopath:posts', $sourcePost),
				];

				$postRepo->sendModeratorActionAlert($sourcePost, 'copy', $this->alertReason, $alertExtras);
			}
		}
	}

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

		if ($this->log)
		{
			$this->app->logger()->logModeratorAction(
				'thread',
				$target,
				'post_copy_target' . ($this->existingTarget ? '_existing' : ''),
				['ids' => implode(', ', $postIds)]
			);

			foreach ($this->sourceThreads AS $sourceThread)
			{
				$this->app->logger()->logModeratorAction('thread', $sourceThread, 'post_copy_source', [
					'url' => $this->app->router('public')->buildLink('nopath:threads', $target),
					'title' => $target->title,
				]);
			}
		}
	}
}
