<?php

namespace XF\Entity;

use XF\Draft;
use XF\Finder\ThreadPrefixFinder;
use XF\ForumType\AbstractHandler;
use XF\Job\ForumDelete;
use XF\Mvc\Entity\ArrayCollection;
use XF\Mvc\Entity\Structure;
use XF\Phrase;
use XF\Repository\OptionRepository;
use XF\Repository\ThreadFieldRepository;

/**
 * COLUMNS
 * @property int $node_id
 * @property int $discussion_count
 * @property int $message_count
 * @property int $last_post_id
 * @property int $last_post_date
 * @property int $last_post_user_id
 * @property string $last_post_username
 * @property int $last_thread_id
 * @property string $last_thread_title
 * @property int $last_thread_prefix_id
 * @property bool $moderate_threads
 * @property bool $moderate_replies
 * @property string $forum_type_id
 * @property array $type_config_
 * @property bool $allow_posting
 * @property bool $count_messages
 * @property bool $auto_feature
 * @property bool $find_new
 * @property string $allow_index
 * @property array $index_criteria
 * @property bool $require_prefix
 * @property string $allowed_watch_notifications
 * @property array $field_cache
 * @property array $prefix_cache
 * @property array $prompt_cache
 * @property int $default_prefix_id
 * @property string $default_sort_order
 * @property string $default_sort_direction
 * @property int $list_date_limit_days
 * @property int $min_tags
 *
 * GETTERS
 * @property-read Draft $draft_thread
 * @property-read ArrayCollection|ThreadPrefix[] $prefixes
 * @property-read Phrase $thread_prompt
 * @property-read AbstractHandler $TypeHandler
 * @property array $type_config
 * @property-read string|null $node_name
 * @property-read string|null $title
 * @property-read string|null $description
 * @property-read int $depth
 *
 * RELATIONS
 * @property-read \XF\Mvc\Entity\AbstractCollection<\XF\Entity\ForumRead> $Read
 * @property-read \XF\Mvc\Entity\AbstractCollection<\XF\Entity\ForumWatch> $Watch
 * @property-read \XF\Mvc\Entity\AbstractCollection<\XF\Entity\Draft> $DraftThreads
 * @property-read Post|null $LastPost
 * @property-read User|null $LastPostUser
 * @property-read Thread|null $LastThread
 * @property-read Node|null $Node
 */
class Forum extends AbstractNode
{
	public function canCreateThread(&$error = null)
	{
		if (!$this->allow_posting)
		{
			$error = \XF::phraseDeferred('you_may_not_perform_this_action_because_forum_does_not_allow_posting');

			return false;
		}

		$visitor = \XF::visitor();

		return $visitor->hasNodePermission($this->node_id, 'postThread');
	}

	public function canCreateThreadPreReg()
	{
		if (\XF::visitor()->user_id || $this->canCreateThread())
		{
			return false;
		}

		return \XF::canPerformPreRegAction(
			function () { return $this->canCreateThread(); }
		);
	}

	public function canCreatePoll(&$error = null)
	{
		return $this->TypeHandler->isThreadTypeCreatable('poll', $this);
	}

	public function canViewThreadContent(): bool
	{
		return \XF::visitor()->hasNodePermission($this->node_id, 'viewContent');
	}

	public function canViewDeletedThreads()
	{
		return \XF::visitor()->hasNodePermission($this->node_id, 'viewDeleted');
	}

	public function canViewModeratedThreads()
	{
		return \XF::visitor()->hasNodePermission($this->node_id, 'viewModerated');
	}

	public function canUploadAndManageAttachments()
	{
		$visitor = \XF::visitor();

		return ($visitor->user_id && $visitor->hasNodePermission($this->node_id, 'uploadAttachment'));
	}

	public function canUploadVideos()
	{
		$options = $this->app()->options();

		if (empty($options->allowVideoUploads['enabled']))
		{
			return false;
		}

		$visitor = \XF::visitor();

		return $visitor->hasNodePermission($this->node_id, 'uploadVideo');
	}

	public function canEditTags(?Thread $thread = null, &$error = null)
	{
		if (!$this->app()->options()->enableTagging)
		{
			return false;
		}

		if ($thread)
		{
			if (!$thread->discussion_open && !$thread->canLockUnlock())
			{
				$error = \XF::phraseDeferred('you_may_not_perform_this_action_because_discussion_is_closed');

				return false;
			}
		}

		$visitor = \XF::visitor();

		// if no thread, assume the thread will be owned by this person
		if (!$thread || $thread->user_id == $visitor->user_id)
		{
			if ($visitor->hasNodePermission($this->node_id, 'tagOwnThread'))
			{
				return true;
			}
		}

		if (
			$visitor->hasNodePermission($this->node_id, 'tagAnyThread')
			|| $visitor->hasNodePermission($this->node_id, 'manageAnyTag')
		)
		{
			return true;
		}

		return false;
	}

	public function canWatch(&$error = null)
	{
		return \XF::visitor()->user_id ? true : false;
	}

	public function isUnread()
	{
		if (!$this->discussion_count)
		{
			return false;
		}

		$cutOff = \XF::$time - $this->app()->options()->readMarkingDataLifetime * 86400;
		if ($this->last_post_date < $cutOff)
		{
			return false;
		}

		$visitor = \XF::visitor();
		if ($visitor->user_id)
		{
			$read = $this->Read[$visitor->user_id];

			return (!$read || $read->forum_read_date < $this->last_post_date);
		}
		else
		{
			return true;
		}
	}

	public function isSearchEngineIndexable(): bool
	{
		if ($this->allow_index == 'deny')
		{
			return false;
		}

		return true;
	}

	/**
	 * Determines if the given thread meets the search engine indexability criteria for this forum.
	 * This is primarily to make it easier to check a thread's default state, ignoring any override.
	 * Note that the thread being checked doesn't have to be in this forum and could be used as a
	 * hypothetical.
	 *
	 * @param Thread $thread
	 *
	 * @return bool
	 */
	public function doesThreadMeetSearchEngineIndexCriteria(Thread $thread): bool
	{
		if ($this->allow_index == 'criteria')
		{
			$criteria = $this->index_criteria;

			if (!empty($criteria['is_sticky']) && $thread->sticky)
			{
				return true;
			}

			if (
				!empty($criteria['is_article'])	&&
				$thread->discussion_type == 'article'
			)
			{
				return true;
			}

			if (
				!empty($criteria['is_solved_question']) &&
				$thread->discussion_type == 'question' &&
				$thread->type_data['solution_post_id']
			)
			{
				return true;
			}

			if (
				!empty($criteria['first_post_staff']) &&
				$thread->User &&
				$thread->User->is_staff
			)
			{
				return true;
			}

			if (
				!empty($criteria['first_post_groups']) &&
				$thread->User &&
				$thread->User->isMemberof([$criteria['first_post_groups']])
			)
			{
				return true;
			}

			if (
				!empty($criteria['max_days_post']) &&
				$thread->post_date < \XF::$time - $criteria['max_days_post'] * 86400
			)
			{
				return false;
			}

			if (
				!empty($criteria['max_days_last_post']) &&
				$thread->last_post_date < \XF::$time - $criteria['max_days_last_post'] * 86400
			)
			{
				return false;
			}

			if (
				!empty($criteria['min_replies']) &&
				$thread->reply_count < $criteria['min_replies']
			)
			{
				return false;
			}

			if (
				isset($criteria['min_reaction_score']) &&
				$thread->first_post_reaction_score < $criteria['min_reaction_score']
			)
			{
				return false;
			}

			if (
				!empty($criteria['min_word_count']) &&
				$thread->getFirstPostWordCount() < $criteria['min_word_count']
			)
			{
				return false;
			}

			return true;
		}

		return ($this->allow_index == 'allow');
	}

	/**
	 * @param bool $current If true, gets the current value; else gets the previous value
	 * @param bool $fallback If true, falls back to the basic forum type if the type is invalid
	 *
	 * @return AbstractHandler
	 */
	public function getTypeHandler(bool $current = true, bool $fallback = true)
	{
		$forumTypeId = $current ? $this->forum_type_id : $this->getExistingValue('forum_type_id');

		$forumType = $this->app()->forumType($forumTypeId, false);
		if (!$forumType && $fallback)
		{
			$forumType = $this->app()->forumType('discussion');
		}

		return $forumType;
	}

	/**
	 * @return array
	 */
	public function getTypeConfig()
	{
		$config = $this->TypeHandler->getDefaultTypeConfig();
		return array_replace($config, $this->type_config_);
	}

	/**
	 * @return array
	 */
	public function getCreatableThreadTypes()
	{
		return $this->TypeHandler->getCreatableThreadTypes($this);
	}

	/**
	 * @param string  $type
	 *
	 * @return bool
	 */
	public function isThreadTypeCreatable($type)
	{
		return $this->TypeHandler->isThreadTypeCreatable($type, $this);
	}

	/**
	 * @return Draft
	 */
	public function getDraftThread()
	{
		return Draft::createFromEntity($this, 'DraftThreads');
	}

	public function getUsablePrefixes(?ThreadPrefix $forcePrefix = null)
	{
		$prefixes = $this->prefixes;

		$prefixes = $prefixes->filter(function ($prefix) use ($forcePrefix)
		{
			if ($forcePrefix && $forcePrefix->prefix_id == $prefix->prefix_id)
			{
				return true;
			}

			return $this->isPrefixUsable($prefix);
		});

		return $prefixes->groupBy('prefix_group_id');
	}

	public function getPrefixesGrouped()
	{
		return $this->prefixes->groupBy('prefix_group_id');
	}

	/**
	 * @return ArrayCollection|ThreadPrefix[]
	 */
	public function getPrefixes()
	{
		if (!$this->prefix_cache)
		{
			return $this->_em->getEmptyCollection();
		}

		$prefixes = $this->finder(ThreadPrefixFinder::class)
			->where('prefix_id', $this->prefix_cache)
			->order('materialized_order')
			->fetch();

		return $prefixes;
	}

	public function isPrefixUsable($prefix, ?User $user = null)
	{
		if (!$this->isPrefixValid($prefix))
		{
			return false;
		}

		if (!($prefix instanceof ThreadPrefix))
		{
			$prefix = $this->em()->find(ThreadPrefix::class, $prefix);
			if (!$prefix)
			{
				return false;
			}
		}

		return $prefix->isUsableByUser($user);
	}

	public function isPrefixValid($prefix)
	{
		if ($prefix instanceof ThreadPrefix)
		{
			$prefix = $prefix->prefix_id;
		}

		return (!$prefix || isset($this->prefix_cache[$prefix]));
	}

	public function getNodeListExtras()
	{
		if (\XF::visitor()->hasNodePermission($this->node_id, 'viewOthers'))
		{
			$output = [
				'discussion_count' => $this->discussion_count,
				'message_count'    => $this->message_count,
				'hasNew'           => $this->isUnread(),
			];

			if ($this->last_post_date)
			{
				$output['last_post_id'] = $this->last_post_id;
				$output['last_post_date'] = $this->last_post_date;
				$output['last_post_user_id'] = $this->last_post_user_id;
				$output['last_post_username'] = $this->last_post_username;
				$output['last_thread_id'] = $this->last_thread_id;
				$output['last_thread_title'] = $this->app()->stringFormatter()->censorText($this->last_thread_title);
				$output['last_thread_prefix_id'] = $this->last_thread_prefix_id;
				$output['LastPostUser'] = $this->LastPostUser;
				$output['LastThread'] = $this->LastThread;
			}

			return $output;
		}
		else
		{
			return ['privateInfo' => true];
		}
	}

	public function getNodeTemplateRenderer($depth)
	{
		return [
			'template' => 'node_list_forum',
			'macro'    => $depth <= 2 ? 'depth' . $depth : 'depthN',
		];
	}

	public function getNewContentState(?Thread $thread = null)
	{
		$visitor = \XF::visitor();

		if ($visitor->user_id && $visitor->hasNodePermission($this->node_id, 'approveUnapprove'))
		{
			return 'visible';
		}

		if (!$visitor->hasPermission('general', 'submitWithoutApproval'))
		{
			return 'moderated';
		}

		if ($thread)
		{
			return $this->moderate_replies ? 'moderated' : 'visible';
		}
		else
		{
			return $this->moderate_threads ? 'moderated' : 'visible';
		}
	}

	public function getNewThread()
	{
		$thread = $this->_em->create(Thread::class);
		$thread->node_id = $this->node_id;

		return $thread;
	}

	public function threadAdded(Thread $thread)
	{
		if ($thread->discussion_type == 'redirect')
		{
			return;
		}

		$this->discussion_count++;
		$this->message_count += 1 + $thread->reply_count;

		if ($thread->last_post_date >= $this->last_post_date)
		{
			$this->last_post_date = $thread->last_post_date;
			$this->last_post_id = $thread->last_post_id;
			$this->last_post_user_id = $thread->last_post_user_id;
			$this->last_post_username = $thread->last_post_username;
			$this->last_thread_id = $thread->thread_id;
			$this->last_thread_title = $thread->title;
			$this->last_thread_prefix_id = $thread->prefix_id;
		}
	}

	public function threadDataChanged(Thread $thread)
	{
		$isRedirect = $thread->discussion_type == 'redirect';
		$wasRedirect = $thread->getExistingValue('discussion_type') == 'redirect';

		if ($isRedirect && !$wasRedirect)
		{
			// this is like the thread being deleted for counter purposes
			$this->threadRemoved($thread);
		}
		else if (!$isRedirect && $wasRedirect)
		{
			// like being added
			$this->threadAdded($thread);
		}
		else if ($isRedirect)
		{
			return;
		}

		$this->message_count += $thread->reply_count - $thread->getExistingValue('reply_count');

		if ($thread->last_post_date >= $this->last_post_date)
		{
			$this->last_post_date = $thread->last_post_date;
			$this->last_post_id = $thread->last_post_id;
			$this->last_post_user_id = $thread->last_post_user_id;
			$this->last_post_username = $thread->last_post_username;
			$this->last_thread_id = $thread->thread_id;
			$this->last_thread_title = $thread->title;
			$this->last_thread_prefix_id = $thread->prefix_id;
		}
		else if ($thread->getExistingValue('last_post_id') == $this->last_post_id)
		{
			$this->rebuildLastPost();
		}
	}

	public function threadRemoved(Thread $thread)
	{
		if ($thread->discussion_type == 'redirect')
		{
			// if this was changed, it used to count so we need to continue
			if (!$thread->isChanged('discussion_type'))
			{
				return;
			}
		}

		$this->discussion_count--;
		$this->message_count -= 1 + $thread->reply_count;

		if ($thread->last_post_id == $this->last_post_id)
		{
			$this->rebuildLastPost();
		}
	}

	public function rebuildCounters()
	{
		$counters = $this->db()->fetchRow("
			SELECT COUNT(*) AS discussion_count,
				COUNT(*) + COALESCE(SUM(reply_count), 0) AS message_count
			FROM xf_thread
			WHERE node_id = ?
				AND discussion_state = 'visible'
				AND discussion_type <> 'redirect'
		", $this->node_id);

		$this->discussion_count = $counters['discussion_count'];
		$this->message_count = $counters['message_count'];

		$this->rebuildLastPost();
	}

	public function rebuildLastPost()
	{
		$thread = $this->db()->fetchRow("
			SELECT *
			FROM xf_thread
			WHERE node_id = ?
				AND discussion_state = 'visible'
				AND discussion_type <> 'redirect'
			ORDER BY last_post_date DESC
			LIMIT 1
		", $this->node_id);
		if ($thread)
		{
			$this->last_post_id = $thread['last_post_id'];
			$this->last_post_date = $thread['last_post_date'];
			$this->last_post_user_id = $thread['last_post_user_id'];
			$this->last_post_username = $thread['last_post_username'];
			$this->last_thread_id = $thread['thread_id'];
			$this->last_thread_title = $thread['title'];
			$this->last_thread_prefix_id = $thread['prefix_id'];
		}
		else
		{
			$this->last_post_id = 0;
			$this->last_post_date = 0;
			$this->last_post_user_id = 0;
			$this->last_post_username = '';
			$this->last_thread_id = 0;
			$this->last_thread_title = '';
			$this->last_thread_prefix_id = 0;
		}
	}

	protected function _preSave()
	{
		if ($this->isChanged('default_sort_order'))
		{
			$sortOrders = $this->TypeHandler->getThreadListSortOptions($this, true);
			if (!isset($sortOrders[$this->default_sort_order]))
			{
				$this->error(\XF::phrase('please_select_valid_default_sort_order'), 'default_sort_order');
			}
		}
		else if ($this->isChanged('forum_type_id'))
		{
			$sortOrders = $this->TypeHandler->getThreadListSortOptions($this, true);
			if (!isset($sortOrders[$this->default_sort_order]))
			{
				// we're changing the type but the selected order is no longer valid, so switch to the first available
				$this->default_sort_order = key($sortOrders);
			}
		}
	}

	protected function _postDelete()
	{
		$this->db()->delete('xf_forum_prefix', 'node_id = ?', $this->node_id);
		$this->db()->delete('xf_forum_watch', 'node_id = ?', $this->node_id);

		if ($this->getOption('delete_threads'))
		{
			$this->app()->jobManager()->enqueueUnique('forumDelete' . $this->node_id, ForumDelete::class, [
				'node_id' => $this->node_id,
			]);
		}

		if ($this->node_id == \XF::options()->reportIntoForumId)
		{
			$this->repository(OptionRepository::class)->updateOption('reportIntoForumId', 0);
		}
	}

	public function getNodeTypeApiData($verbosity = self::VERBOSITY_NORMAL, array $options = [])
	{
		$result = parent::getNodeTypeApiData();

		if (\XF::visitor()->hasNodePermission($this->node_id, 'viewOthers'))
		{
			$result->includeExtra([
				'is_unread' => $this->isUnread(),
				'discussion_count' => $this->discussion_count,
				'message_count' => $this->message_count,
				'last_post_id' => $this->last_post_id,
				'last_post_date' => $this->last_post_date,
				'last_post_username' => $this->last_post_username,
				'last_thread_id' => $this->last_thread_id,
				'last_thread_title' => $this->app()->stringFormatter()->censorText($this->last_thread_title),
				'last_thread_prefix_id' => $this->last_thread_prefix_id,
			]);
		}

		$this->TypeHandler->addTypeConfigToApiResult($this, $result, $verbosity, $options);

		if ($verbosity > self::VERBOSITY_NORMAL)
		{
			$result->prefixes = $this->prefixes->toApiResults();

			$fields = [];
			if ($this->field_cache)
			{
				$fieldEntities = $this->repository(ThreadFieldRepository::class)->findFieldsForList()
					->whereIds($this->field_cache)
					->fetch();
				$fields = $fieldEntities->toApiResults();
			}

			$result->custom_fields = $fields;
		}

		$result->can_create_thread = $this->canCreateThread();
		$result->can_upload_attachment = $this->canUploadAndManageAttachments();

		return $result;
	}

	public static function getStructure(Structure $structure)
	{
		$structure->table = 'xf_forum';
		$structure->shortName = 'XF:Forum';
		$structure->contentType = 'forum';
		$structure->primaryKey = 'node_id';
		$structure->columns = [
			'node_id'                     => ['type' => self::UINT, 'required' => true],
			'discussion_count'            => ['type' => self::UINT, 'forced' => true, 'default' => 0],
			'message_count'               => ['type' => self::UINT, 'forced' => true, 'default' => 0],
			'last_post_id'                => ['type' => self::UINT, 'default' => 0],
			'last_post_date'              => ['type' => self::UINT, 'default' => 0],
			'last_post_user_id'           => ['type' => self::UINT, 'default' => 0],
			'last_post_username'          => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
			'last_thread_id'              => ['type' => self::UINT, 'default' => 0],
			'last_thread_title'           => ['type' => self::STR, 'maxLength' => 150, 'default' => ''],
			'last_thread_prefix_id'       => ['type' => self::UINT, 'default' => 0],
			'moderate_threads'            => ['type' => self::BOOL, 'default' => false],
			'moderate_replies'            => ['type' => self::BOOL, 'default' => false],
			'forum_type_id'               => ['type' => self::STR, 'default' => 'discussion', 'api' => true],
			'type_config'                 => ['type' => self::JSON_ARRAY, 'default' => []],
			'allow_posting'               => ['type' => self::BOOL, 'default' => true, 'api' => true],
			'count_messages'              => ['type' => self::BOOL, 'default' => true],
			'auto_feature'                => ['type' => self::BOOL, 'default' => false],
			'find_new'                    => ['type' => self::BOOL, 'default' => true],
			'allow_index'                 => ['type' => self::STR, 'default' => 'allow',
				'allowedValues' => ['allow', 'deny', 'criteria'],
			],
			'index_criteria'              => ['type' => self::JSON_ARRAY, 'default' => []],
			'require_prefix'              => ['type' => self::BOOL, 'default' => false, 'api' => true],
			'allowed_watch_notifications' => ['type' => self::STR, 'default' => 'all',
				'allowedValues' => ['all', 'thread', 'none'],
			],
			'field_cache'                 => ['type' => self::JSON_ARRAY, 'default' => []],
			'prefix_cache'                => ['type' => self::JSON_ARRAY, 'default' => []],
			'prompt_cache'                => ['type' => self::JSON_ARRAY, 'default' => []],
			'default_prefix_id'           => ['type' => self::UINT, 'default' => 0],
			'default_sort_order'          => ['type' => self::STR, 'default' => 'last_post_date'],
			'default_sort_direction'      => ['type' => self::STR, 'default' => 'desc',
				'allowedValues' => ['asc', 'desc'],
			],
			'list_date_limit_days'        => ['type' => self::UINT, 'default' => 0, 'max' => 3650],
			'min_tags'                    => ['type' => self::UINT, 'default' => 0, 'max' => 100, 'api' => true],
		];
		$structure->getters = [
			'draft_thread'  => true,
			'prefixes'      => true,
			'thread_prompt' => true,
			'TypeHandler' => [
				'cache' => true,
				'getter' => 'getTypeHandler',
				'invalidate' => ['forum_type_id'],
			],
			'type_config' => [
				'cache' => true,
				'getter' => 'getTypeConfig',
				'invalidate' => ['forum_type_id'],
			],
		];
		$structure->relations = [
			'Read'   => [
				'entity'     => 'XF:ForumRead',
				'type'       => self::TO_MANY,
				'conditions' => 'node_id',
				'key'        => 'user_id',
			],
			'Watch'  => [
				'entity'     => 'XF:ForumWatch',
				'type'       => self::TO_MANY,
				'conditions' => 'node_id',
				'key'        => 'user_id',
			],
			'DraftThreads' => [
				'entity'     => 'XF:Draft',
				'type'       => self::TO_MANY,
				'conditions' => [
					['draft_key', '=', 'forum-', '$node_id'],
				],
				'key'        => 'user_id',
			],
			'LastPost' => [
				'entity' => 'XF:Post',
				'type' => self::TO_ONE,
				'conditions' => [['post_id', '=', '$last_post_id']],
				'primary' => true,
			],
			'LastPostUser' => [
				'entity'     => 'XF:User',
				'type'       => self::TO_ONE,
				'conditions' => [['user_id', '=', '$last_post_user_id']],
				'primary'    => true,
			],
			'LastThread' => [
				'entity'     => 'XF:Thread',
				'type'       => self::TO_ONE,
				'conditions' => [['thread_id', '=', '$last_thread_id']],
				'primary'    => true,
			],
		];

		$structure->options = [
			'delete_threads' => true,
		];

		$structure->withAliases = [
			'api' => [
				function ()
				{
					$userId = \XF::visitor()->user_id;
					if ($userId)
					{
						return [
							'Read|' . $userId,
						];
					}
				},
			],
		];

		static::addDefaultNodeElements($structure);

		return $structure;
	}

	public static function getListedWith()
	{
		$visitor = \XF::visitor();
		$with = ['LastPostUser', 'LastThread'];

		if ($visitor->user_id)
		{
			$with[] = "Read|{$visitor->user_id}";
			$with[] = "LastThread.Read|{$visitor->user_id}";
		}

		return $with;
	}

	/**
	 * @return Phrase
	 */
	public function getThreadPrompt()
	{
		static $phraseName; // always return the same phrase for the same forum instance

		if (!$phraseName)
		{
			if ($this->prompt_cache)
			{
				$phraseName = 'thread_prompt.' . array_rand($this->prompt_cache);
			}
			else
			{
				$phraseName = 'thread_prompt.default';
			}
		}

		return \XF::phrase($phraseName);
	}
}
