<?php
/**
 * @brief		Helpful Trait
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @since		12 Jul 2023
 */

namespace IPS\Content;

/* To prevent PHP errors (extending class does not exist) revealing path */

use BadMethodCallException;
use DomainException;
use Exception;
use IPS\Application;
use IPS\DateTime;
use IPS\Db;
use IPS\IPS;
use IPS\Member;
use IPS\Platform\Bridge;
use IPS\Redis;
use RedisException;
use UnderflowException;
use function array_key_exists;
use function defined;
use function get_class;
use function iterator_to_array;
use function strtolower;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0') . ' 403 Forbidden' );
	exit;
}

/**
 * Helpful Trait
 */
trait Helpful
{
	/**
	 * @var array|null
	 */
	protected ?array $_helpfulData = null;

	/**
	 * @return array
	 */
	public function get_helpful() : array
	{
		if( $this->_helpfulData === null )
		{
			$this->_helpfulData = [
				'marked_by' => array_keys( $this->helpfuls() ),
				'count' => $this->helpfulCount()
			];
		}

		return $this->_helpfulData;
	}

	/**
	 * @param array|null $val
	 * @return void
	 */
	public function set_helpful( ?array $val ) : void
	{
		$this->_helpfulData = $val;
	}

	/**
	 * Mark Helpful
	 *
	 * @param Member|null $member The member giving mark, or NULL
	 * @return    void
	 * @throws    DomainException
	 * @throws Exception
	 */
	public function markHelpful( ?Member $member = NULL ): void
	{
		/* Did we pass a member? */
		$member = $member ?: Member::loggedIn();

		/* Can we mark helpful? */
		if ( !$this->canView( $member ) or !$this->canMarkHelpful() )
		{
			throw new DomainException( 'cannot_mark_helpful' );
		}
		
		/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
		$app = explode( '\\', get_class( $this ) );
		if ( strtolower( $app[1] ) === $app[1] )
		{
			$app = $app[1];
		}
		else
		{
			$app = 'core';
		}
		
		$itemClass = $this::$itemClass;
		$itemIdField = $itemClass::$databaseColumnId;
		$idColumn = static::$databaseColumnId;
		
		Db::i()->insert( 'core_solved_index', array(
			'member_id'				=> $this->author()->member_id,
			'app'					=> $app,
			'solved_date'			=> DateTime::create()->getTimestamp(),
			'member_given'			=> $member->member_id,
			'comment_class'			=> get_class( $this ),
			'comment_id'			=> $this->$idColumn,
			'item_id'				=> $this->item()->$itemIdField,
			'type'					=> 'helpful',
			'node_id'				=> $this->item()->container()->_id
		) );

		/* If we have a mapping, increment that column */
		if( isset( static::$databaseColumnMap['num_helpful'] ) )
		{
			$helpfulCountColumn = static::$databaseColumnMap['num_helpful'];
			$this->$helpfulCountColumn++;
			$this->save();
		}

		/* And if this is a comment, check if item has a mapping */
		$item = $this->item();
		if( isset( $item::$databaseColumnMap[ 'num_helpful' ] ) )
		{
			$helpfulCountColumn = $item::$databaseColumnMap[ 'num_helpful' ];
			$item->$helpfulCountColumn++;
			$item->save();
		}

		/* Trending */
		if( Application::appIsEnabled( 'cloud' ) and Bridge::i()->featureIsEnabled( 'trending' ) )
		{
			$item = $this->item();

			$itemIdColumn	= $item::$databaseColumnId;
			$itemId			= $item->$itemIdColumn;
			try
			{
				Redis::i()->zIncrBy( 'trending', time() * 0.5, get_class( $item ) .'__' . $itemId );
			}
			catch( BadMethodCallException | RedisException ) {}
		}

		/* Reset some cached values */
		$this->_helpfulCount	= NULL;
		$this->_helpfuls		= NULL;

		$this->hasMarkedHelpful[ $member->member_id ] = true;

		if ( IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
		{
			$this->item()->clearCachedStatistics();
		}

		/* Achievements */
		$this->author()->achievementAction( 'forums', 'AnswerMarkedBest', [ 'post' => $this, 'type' => 'helpful' ] );
	}
	
	/**
	 * Unmark Helpful
	 *
	 * @param	Member|NULL		$member					The member, or NULL for currently logged in member
	 * @return	void
	 */
	public function unmarkHelpful( ?Member $member = NULL ): void
	{
		$member = $member ?: Member::loggedIn();
		$item = $this->item();

		if ( !$this->canView( $member ) or !$this->canMarkHelpful() )
		{
			throw new DomainException( 'cannot_mark_helpful' );
		}

		$idColumn = static::$databaseColumnId;
		Db::i()->delete( 'core_solved_index', array( "app=? AND comment_id=? AND member_given=? AND type=?", static::$application, $this->$idColumn, $member->member_id, 'helpful' ) );

		/* Reset some cached values */
		$this->_helpfulsCount	= NULL;
		$this->helpfuls		= NULL;

		if( isset( $this->hasMarkedHelpful[ $member->member_id ] ) )
		{
			unset( $this->hasMarkedHelpful[ $member->member_id ] );
		}

		/* If we have a mapping, decrement that column */
		if( isset( static::$databaseColumnMap['num_helpful'] ) )
		{
			$helpfulCountColumn = static::$databaseColumnMap['num_helpful'];
			$this->$helpfulCountColumn--;
			$this->save();
		}
		
		if( isset( $item::$databaseColumnMap[ 'num_helpful' ] ) )
		{
			$helpfulCountColumn = $item::$databaseColumnMap[ 'num_helpful' ];
			$item->$helpfulCountColumn--;
			$item->save();
		}

		if ( IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
		{
			$item->clearCachedStatistics();
		}
	}

	/**
	 * @brief	Helpfuls Cache
	 */
	protected array|null $_helpfuls = null;

	/**
	 * @brief Helpful Count
	 */
	protected int|null $_helpfulCount = null;

	/**
	 * Number of helpful comments per item
	 */
	protected int|null $_helpfulRepliesCount = null;

	/**
	 * Get the number of comments marked helpful in this item
	 * @note We could probably cache this in the topic row, but it's a quick and efficient query
	 *
	 * @return	int
	 */
	public function helpfulsRepliesCount(): int
	{
		if( isset( static::$databaseColumnMap['num_helpful'] ) )
		{
			return $this->mapped( 'num_helpful' );
		}

		if ( $this->_helpfulRepliesCount === NULL )
		{
			$idColumn = static::$databaseColumnId;
			$this->_helpfulRepliesCount = (int) Db::i()->select( 'COUNT( DISTINCT(comment_id) )', 'core_solved_index', ["app=? AND item_id=? AND type=? AND hidden=0", static::$application, $this->$idColumn, 'helpful'] )->first();
		}

		return $this->_helpfulRepliesCount;
	}

	/**
	 * Recount the total helpful replies
	 *
	 * @return void
	 */
	public function recountHelpfuls() : void
	{
		if( isset( static::$databaseColumnMap['num_helpful'] ) )
		{
			$idColumn = static::$databaseColumnId;
			$helpfulColumn = static::$databaseColumnMap['num_helpful'];
			$this->$helpfulColumn = (int) Db::i()->select( 'COUNT( DISTINCT(comment_id) )', 'core_solved_index', [ 'app=? and item_id=? and type=? and hidden=?', static::$application, $this->$idColumn, 'helpful', 0 ], flags: Db::SELECT_FROM_WRITE_SERVER )->first();
			$this->save();
		}
	}

	/**
	 * Helpful Count
	 *
	 * @return int
	 */
	public function helpfulCount(): int
	{
		$this->helpfuls();

		return $this->_helpfulCount;
	}

	/**
	 * @brief	Cached Reacted
	 */
	protected array $hasMarkedHelpful = array();

	/**
	 * Has marked helpful?
	 *
	 * @param	Member|NULL	$member	The member, or NULL for currently logged in
	 * @return    bool
	 */
	public function markedHelpful( ?Member $member = NULL ): bool
	{
		$member = $member ?: Member::loggedIn();

		if( is_array( $this->helpful ) )
		{
			/* This may be set in Item::_comments() */
			$this->hasMarkedHelpful[ $member->member_id ] = in_array( $member->member_id, $this->helpful['marked_by'] );
		}
		else if( !isset( $this->hasMarkedHelpful[ $member->member_id ] ) )
		{
			$this->hasMarkedHelpful[ $member->member_id ] = FALSE;

			try
			{
				$idColumn = static::$databaseColumnId;
				$this->hasMarkedHelpful[ $member->member_id ] = (bool) Db::i()->select( 'id', 'core_solved_index', array( "app=? AND comment_id=? AND member_given=? AND type=? AND hidden=0", static::$application, $this->$idColumn, $member->member_id, 'helpful' )  )->first();
			}
			catch( UnderflowException ){}
		}

		return $this->hasMarkedHelpful[ $member->member_id ];
	}

	/**
	 * Helpfuls
	 *
	 * @return	array
	 */
	public function helpfuls(): array
	{
		if ( $this->_helpfulCount === NULL )
		{
			$this->_helpfulCount = 0;
		}

		if ( $this->_helpfuls === NULL )
		{
			$this->_helpfuls = array();

			$idColumn = static::$databaseColumnId;
			foreach( Db::i()->select( '*', 'core_solved_index', array( array( "app=? AND comment_id=? AND type=? and hidden=0", static::$application, $this->$idColumn, 'helpful' ) ) ) as $helpful )
			{
				$this->_helpfuls[ $helpful['member_given'] ] = true;
				$this->_helpfulCount += 1;
			}
		}

		return $this->_helpfuls;
	}

	/**
	 * Can user mark this item helpful?
	 *
	 * @param Member|null $member The member (null for currently logged in member)
	 * @return    bool
	 */
	public function canMarkHelpful( ?Member $member = NULL ): bool
	{
		/* Extensions go first */
		if( $permCheck = Permissions::can( 'helpful', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}
		$member = $member ?: Member::loggedIn();

		/* It's a no for guests */
		if ( ! $member->member_id )
		{
			return FALSE;
		}

		if( isset( static::$archiveClass ) AND method_exists( $this, 'isArchived' ) AND $this->isArchived() )
		{
			return FALSE;
		}

		if ( ! $member->group['gbw_can_mark_helpful'] )
		{
			return FALSE;
		}

		if ( $member === $this->author() )
		{
			return FALSE;
		}

		/* Otherwise yes */
		return TRUE;
	}

	/*
	 * @brief Members who are an expert in this node
	 */
	protected static array $expertsInThisNode = [];

	/**
	 * Is the author an expert in this node?
	 *
	 * @return bool
	 */
	public function authorIsAnExpert() : bool
	{
		/* Do they have it disabled? */
		if( !Bridge::i()->featureIsEnabled( 'experts' )  or $this->author()->members_bitoptions['expert_user_disabled'] or !$this->author()->isExpert() )
		{
			return false;
		}
		
		$container = $this->item()->container();

		if( !array_key_exists( $container->_id, static::$expertsInThisNode ) )
		{
			static::$expertsInThisNode[ $container->_id ] = iterator_to_array( Db::i()->select( 'member_id', 'core_expert_users', [ 'node_id=?', $container->_id ] ) );
		}

		return in_array( $this->author()->member_id, static::$expertsInThisNode[ $container->_id ] );
	}

	/**
	 * The item contains 1 or more helpful comments
	 *
	 * @param Item $item
	 * @return bool
	 */
	public static function itemHasHelpful( Item $item ): bool
	{
		if( isset( $item::$databaseColumnMap['num_helpful'] ) )
		{
			return (bool) $item->mapped( 'num_helpful' );
		}

		try
		{
			return (bool) $item->helpfulPosts(1);
		}
		catch( Exception ) {}

		return false;
	}
}