<?php

namespace XF\Service\Thread;

use XF\App;
use XF\Entity\Post;
use XF\Entity\Thread;
use XF\Entity\User;
use XF\PrintableException;
use XF\Service\AbstractService;
use XF\Service\Post\NotifierService;
use XF\Service\Post\PreparerService;
use XF\Service\ValidateAndSavableTrait;
use XF\Validator\Username;

class ReplierService extends AbstractService
{
	use ValidateAndSavableTrait;

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

	/**
	 * @var Post
	 */
	protected $post;

	/**
	 * @var User
	 */
	protected $user;

	/**
	 * @var PreparerService
	 */
	protected $postPreparer;

	protected $performValidations = true;

	protected $isPreRegAction = false;

	/**
	 * @var float
	 */
	protected $notifyRunTime = 0.25;

	public function __construct(App $app, Thread $thread)
	{
		parent::__construct($app);
		$this->setThread($thread);
		$this->setUser(\XF::visitor());
		$this->setPostDefaults();
	}

	protected function setThread(Thread $thread)
	{
		$this->thread = $thread;
		$this->post = $thread->getNewPost();
		$this->postPreparer = $this->service(PreparerService::class, $this->post);
	}

	/**
	 * @return Thread
	 */
	public function getThread()
	{
		return $this->thread;
	}

	public function getPost()
	{
		return $this->post;
	}

	public function getPostPreparer()
	{
		return $this->postPreparer;
	}

	protected function setUser(User $user)
	{
		$this->user = $user;
	}

	public function logIp($logIp)
	{
		$this->postPreparer->logIp($logIp);
	}

	public function setPerformValidations($perform)
	{
		$this->performValidations = (bool) $perform;
	}

	public function getPerformValidations()
	{
		return $this->performValidations;
	}

	public function setIsAutomated()
	{
		$this->logIp(false);
		$this->setPerformValidations(false);
	}

	public function setIsPreRegAction(bool $isPreRegAction)
	{
		$this->isPreRegAction = $isPreRegAction;
	}

	public function setNotifyRunTime(float $time): void
	{
		$this->notifyRunTime = $time;
	}

	protected function setPostDefaults()
	{
		$forum = $this->thread->Forum;

		if (!$forum)
		{
			throw new \LogicException("Thread is not in a valid forum");
		}

		$this->post->message_state = $forum->getNewContentState($this->thread);
		$this->post->user_id = $this->user->user_id;
		$this->post->username = $this->user->username;
	}

	public function setMessage($message, $format = true)
	{
		return $this->postPreparer->setMessage($message, $format, $this->performValidations);
	}

	public function setAttachmentHash($hash)
	{
		$this->postPreparer->setAttachmentHash($hash);
	}

	public function checkForSpam()
	{
		if ($this->post->message_state == 'visible' && $this->user->isSpamCheckRequired())
		{
			$this->postPreparer->checkForSpam();
		}
	}

	protected function finalSetup()
	{
		$this->post->post_date = time();
	}

	protected function _validate()
	{
		$this->finalSetup();

		$post = $this->post;

		if (!$post->user_id && !$this->isPreRegAction)
		{
			/** @var Username $validator */
			$validator = $this->app->validator(Username::class);
			$post->username = $validator->coerceValue($post->username);
			if ($this->performValidations && !$validator->isValid($post->username, $error))
			{
				return [$validator->getPrintableErrorValue($error)];
			}
		}
		else if ($this->isPreRegAction && !$post->username)
		{
			// need to force a value here to avoid a presave error
			$post->username = 'preRegAction-' . \XF::$time;
		}

		$post->preSave();

		$errors = $post->getErrors();

		return $errors;
	}

	protected function _save()
	{
		if ($this->isPreRegAction)
		{
			throw new \LogicException("Pre-reg action replies cannot be saved");
		}

		$post = $this->post;

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

		$threadLatest = $this->db()->fetchRow("
			SELECT *
			FROM xf_thread
			WHERE thread_id = ?
			FOR UPDATE
		", $this->thread->thread_id);

		if (!$threadLatest)
		{
			throw new PrintableException(\XF::phrase('requested_thread_not_found'));
		}

		// Ensure our thread entity has the latest data to make sure things like reply count are correct
		$forceUpdateColumns = [
			'first_post_id',
			'reply_count',
			'last_post_date',
			'last_post_id',
			'last_post_user_id',
			'last_post_username',
		];
		foreach ($forceUpdateColumns AS $forceUpdateColumn)
		{
			$this->thread->setAsSaved($forceUpdateColumn, $threadLatest[$forceUpdateColumn]);
		}

		// In very busy threads, it's possible that this request may execute at a different speed from another
		// and end up with a pending post_date that is less than that of the most recently inserted post which
		// would trigger an error when setting the post position. Attempt to detect this case and adjust
		// the post date to avoid an error if possible.
		$time = max($threadLatest['last_post_date'], time());
		if ($threadLatest['last_post_date'] > $post->post_date)
		{
			$post->set('post_date', $time, ['forceSet' => true]);
		}

		$this->setPostPosition($threadLatest);

		$post->save(true, false);

		$this->postPreparer->afterInsert();

		$db->commit();

		return $post;
	}

	protected function setPostPosition(array $threadInfo)
	{
		$post = $this->post;

		if ($post->post_date < $threadInfo['last_post_date'])
		{
			throw new \LogicException("Replier can only add posts at the end of a thread");
		}

		if ($post->message_state == 'visible')
		{
			$position = $threadInfo['reply_count'] + 1;
		}
		else
		{
			$position = $threadInfo['reply_count'];
		}

		$post->set('position', $position, ['forceSet' => true]);
	}

	public function sendNotifications()
	{
		if ($this->post->isVisible())
		{
			$notifier = $this->service(NotifierService::class, $this->post, 'reply');
			$notifier->setMentionedUserIds($this->postPreparer->getMentionedUserIds());
			$notifier->setQuotedUserIds($this->postPreparer->getQuotedUserIds());
			$notifier->notifyAndEnqueue($this->notifyRunTime);
		}
	}
}
