<?php

/**
 * @brief        Assignable
 * @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
 * @subpackage
 * @since        1/8/2024
 */

namespace IPS\Content;

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

use DomainException;
use IPS\Api\Webhook;
use IPS\core\Assignments\Assignment;
use IPS\Data\Store;
use IPS\Db;
use IPS\Helpers\Form\FormAbstract;
use IPS\Helpers\Form\Text;
use IPS\Member;
use IPS\Member\Team;
use IPS\Node\Model;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Platform\Bridge;
use IPS\Theme;
use OutOfRangeException;
use function count;
use function get_class;
use function is_string;
use function strpos;
use function substr;
use function trim;

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

trait Assignable
{
	/**
	 * Container has assignable enabled
	 *
	 * @param Model $container
	 * @return	bool
	 */
	abstract public static function containerAllowsAssignable( Model $container ): bool;

	/**
	 * Return a list of containers IDs that allow assignments.
	 * We will use this to calculate open assignments
	 * and to build the assignments list.
	 *
	 * @return array
	 */
	abstract public static function assignableContainerIds() : array;

	/**
	 * @return Assignment|null
	 */
	public function get_assignment() : ?Assignment
	{
		/* Do we have a column? */
		if( $this->isAssigned() )
		{
			if( isset( static::$databaseColumnMap['assigned'] ) )
			{
				$column = static::$databaseColumnMap['assigned'];
				try
				{
					return Assignment::load( $this->$column );
				}
				catch( OutOfRangeException ){}
			}
			else
			{
				try
				{
					return Assignment::loadByItem( $this );
				}
				catch( OutOfRangeException ){}
			}
		}

		return null;
	}

	/**
	 * Can user assign this item?
	 *
	 * @param Member|null $member The member (null for currently logged in member)
	 * @return    bool
	 */
	public function canAssign( ?Member $member = NULL ): bool
	{
		if( !Bridge::i()->featureIsEnabled( 'assignments' ) )
		{
			return false;
		}

		/* Extensions go first */
		if( $permCheck = Permissions::can( 'assign', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}

		return static::containerAllowsAssignable( $this->container() ) AND static::modPermission( 'assign', $member, $this->container() );
	}

	/**
	 * Can this member be assigned to this item?
	 *
	 * @param Member $member
	 * @return bool
	 */
	public function canBeAssigned( Member $member ) : bool
	{
		if( !Bridge::i()->featureIsEnabled( 'assignments' ) )
		{
			return false;
		}

		/* The user must have permission to reply to this item */
		if( static::containerAllowsAssignable( $this->container() ) and $this->canComment( $member, false ) )
		{
			/* Make sure the member is a moderator */
			$moderators = static::getModerators( $this->containerWrapper( true ) );
			if( in_array( $member->member_id, $moderators['m'] ) )
			{
				return true;
			}

			return $member->inGroup( $moderators['g'] );
		}

		return false;
	}

	/**
	 * Get current assignment
	 *
	 * @return Member|Team|null
	 */
	public function assignedTo() : Member|Team|null
	{
		try
		{
			return Assignment::loadByItem( $this )->assignedTo();
		}
		catch( OutOfRangeException )
		{
			return null;
		}
	}

	/**
	 * @var array
	 */
	protected static array $_memberAssignments = [];

	/**
	 * Get all items assigned to this member
	 *
	 * @param Member|null $member
	 * @return array
	 */
	protected static function memberAssignments( ?Member $member=null ) : array
	{
		$member = $member ?: Member::loggedIn();
		if( ! isset( static::$_memberAssignments[ $member->member_id ] ) )
		{
			$teamIds = $member->teams() ? array_keys( $member->teams() ): [];
			static::$_memberAssignments[ $member->member_id ] = iterator_to_array(
				new ActiveRecordIterator(
					Db::i()->select( '*', 'core_assignments', [ 'assign_item_class=? and ( ( assign_type=? and assign_to=? ) or ( assign_type=? and assign_to in(?) ) )', get_called_class(), Assignment::ASSIGNMENT_MEMBER, $member->member_id, Assignment::ASSIGNMENT_TEAM, implode( ",", $teamIds ) ] ),
					Assignment::class
				)
			);
		}

		return is_array( static::$_memberAssignments[ $member->member_id ] ) ? static::$_memberAssignments[ $member->member_id ] : [];
	}

	/**
	 * Determines if the item is assigned to this member
	 *
	 * @param Member|null $member
	 * @param bool 	$checkClosed
	 * @return bool
	 */
	public function isAssignedToMember( ?Member $member = null, bool $checkClosed=false ) : bool
	{
		if( !Bridge::i()->featureIsEnabled( 'assignments' ) )
		{
			return false;
		}

		/* If assignments are disabled on the container, this is always false */
		if( !static::containerAllowsAssignable( $this->container() ) )
		{
			return false;
		}

		$member = $member ?: Member::loggedIn();

		$idColumn = static::$databaseColumnId;
		foreach( static::memberAssignments( $member ) as $assignment )
		{
			if( $assignment->item_id == $this->$idColumn )
			{
				if( !$checkClosed and $assignment->closed )
				{
					return false;
				}

				return true;
			}
		}

		/* If it wasn't in the member assignment list, we're not assigned */
		return false;
	}

	/**
	 * Does this item have an assignment?
	 *
	 * @return bool
	 */
	public function isAssigned() : bool
	{
		if( !Bridge::i()->featureIsEnabled( 'assignments' ) )
		{
			return false;
		}

		if( isset( static::$databaseColumnMap['assigned'] ) )
		{
			$column = static::$databaseColumnMap['assigned'];
			return $this->$column !== null;
		}

		try
		{
			Assignment::loadByItem( $this );
			return true;
		}
		catch( OutOfRangeException )
		{
			return false;
		}
	}

	/**
	 * Assign this item to a member or team
	 *
	 * @param Member|Team|string $assignTo
	 * @return Member|Team|bool		FALSE if the assignment did not change
	 */
	public function assign( Member|Team|string $assignTo ) : Member|Team|bool
	{
		try
		{
			/* If we already have an assignment we'll just update it */
			$assignment = Assignment::loadByItem( $this );
		}
		catch( OutOfRangeException )
		{
			$assignment = new Assignment;
			$assignment->item = $this;
		}

		if( is_string( $assignTo ) )
		{
			if( $pos = strpos( $assignTo, ':' ) )
			{
				try
				{
					$assignTo = Team::load( trim( substr( $assignTo, $pos + 1 ) ), 'team_name' );
				}
				catch( OutOfRangeException )
				{
					return false;
				}
			}
			else
			{
				$assignTo = Member::load( $assignTo, 'name' );
				if( !$assignTo->member_id )
				{
					return false;
				}
			}
		}

		/* If the assignment did not change, stop here */
		if( !$assignment->closed AND $assignTo === $assignment->assignedTo() )
		{
			return false;
		}

		/* Reopen the assignment */
		$assignment->closed = 0;
		$assignment->to = $assignTo;
		$assignment->save();

		/* If the item has a column, set the value */
		if( isset( static::$databaseColumnMap['assigned'] ) )
		{
			$column = static::$databaseColumnMap['assigned'];
			$this->$column = $assignment->id;
			$this->save();
		}

		Webhook::fire( 'content_assigned', $assignment );

		return $assignTo;
	}

	/**
	 * Remove the assignment from this item
	 *
	 * @return void
	 */
	public function unassign() : void
	{
		try
		{
			$assignment = Assignment::loadByItem( $this );
			$assignment->close();
			Webhook::fire( 'content_unassigned', $assignment );
		}
		catch( OutOfRangeException ){}
	}

	/**
	 * Log the reply for this item so that we can generate reports
	 *
	 * @param Comment $reply
	 * @return void
	 */
	public function trackReply( Comment $reply ) : void
	{
		try
		{
			Assignment::loadByItem( $this )->logReply( $reply );
		}
		catch( OutOfRangeException ){}
	}

	/**
	 * Get a list of all the moderators
	 *
	 * @param Model|null $container
	 * @return array
	 */
	protected static function getModerators( ?Model $container=null ) : array
	{
		if( $container !== null )
		{
			return $container::moderators( $container );
		}

		return [
			'g' => iterator_to_array( Db::i()->select( 'id', 'core_moderators', [ 'perms=?', '*' ] ) ),
			'm' => []
		];
	}

	/**
	 * Build the member IDs for this container
	 *
	 * @param Model|null $container
	 * @return array
	 */
	public static function getMemberAssignmentOptions( ?Model $container=null ) : array
	{
		try
		{
			$cache = Store::i()->assignmentOptions;
		}
		catch( OutOfRangeException )
		{
			$cache = [];
		}

		$cacheKey = $container ? get_class( $container ) . '__' . $container->_id : '__GLOBAL__';
		if( !isset( $cache[ $cacheKey ] ) )
		{
			$containerModerators = static::getModerators( $container );
			$cache[ $cacheKey ] = iterator_to_array(
				Db::i()->select( 'member_id', 'core_members', ( count( $containerModerators['m'] ) ? Db::i()->in( 'member_id', $containerModerators['m'] ) . ' OR ' : '' ) . Db::i()->in( 'member_group_id', $containerModerators['g'] ) . ' OR ' . Db::i()->findInSet( 'mgroup_others', $containerModerators['g'] ) )
			);

			Store::i()->assignmentOptions = $cache;
		}

		return $cache[ $cacheKey ];
	}


	/**
	 * Generate a field for the assignment options
	 *
	 * @param Item $item
	 * @param Model|null $container
	 * @param string|null	$fieldName	Optional override for the field name
	 * @return FormAbstract
	 */
	public static function assignmentFormField( Item $item, ?Model $container=null, ?string $fieldName=null ) : FormAbstract
	{
		/* Preload some language strings */
		Member::loggedIn()->language()->get( ['assignment_team', 'assign_to'] );

		$prefix = Member::loggedIn()->language()->get( 'assignment_team' );
		$currentValue = null;
		if( $currentAssignment = $item->assignment )
		{
			if( !$currentAssignment->closed and $currentValue = $item->assignedTo() )
			{
				if( $currentValue instanceof Team )
				{
					$currentValue = $prefix . ': ' . $currentValue->name;
				}
				else
				{
					$currentValue = $currentValue->name;
				}
			}
		}

		$options = [
			'autocomplete' => [
				'minimized' => false,
				'maxItems' => 1,
				'forceLower' => false,
				'minAjaxLength' => 1,
				'addTokenText' => Member::loggedIn()->language()->get( 'assign_to' ),
				'addTokenTemplate' => 'core.autocomplete.assignmentAddToken'
			]
		];

		/* Do we have a very large list of options? */
		$assignmentOptions = static::getMemberAssignmentOptions( $container );
		$total = count( $assignmentOptions ) + count( Team::teams() );
		if( $total >= 100 )
		{
			$source = 'app=cloud&module=assignments&controller=assignments&do=findAssignmentOptions&class=' . get_called_class();

			if( $container )
			{
				$source .= '&container=' . $container->_id;
			}

			$options['autocomplete']['freeChoice'] = true;
			$options['autocomplete']['resultItemTemplate'] = 'core.autocomplete.assignmentResultItem';
		}
		else
		{
			$source = [];
			/** @var Member $member */
			foreach( new ActiveRecordIterator(
				Db::i()->select( '*', 'core_members', Db::i()->in( 'member_id', $assignmentOptions ) ),
						 Member::class
					 ) as $member )
			{
				if( $container === null or $container->can( 'reply', $member, false ) )
				{
					$source[ $member->name ] = Theme::i()->getTemplate( 'global', 'core', 'front' )->memberAssignment( $member );
				}
			}

			/* Now teams */
			foreach( Team::teams() as $team )
			{
				$source[ $prefix . ': ' . $team->name ] = Theme::i()->getTemplate( 'global', 'core', 'front' )->teamAssignment( $team );
			}

			$options['autocomplete']['formatSource'] = true;
			$options['autocomplete']['freeChoice'] = false;
			$options['autocomplete']['resultItemTemplate'] = 'core.autocomplete.resultItem';
		}

		$options['autocomplete']['source'] = $source;
		$options['autocomplete']['suggestionsOnly'] = true;

		return new Text( $fieldName ?? 'assign_content_to', $currentValue, false, $options, function( $val ) use ( $item ){
			if( !empty( $val ) )
			{
				/* This is a team */
				if( $pos = strpos( $val, ':' ) )
				{
					return true;
				}

				$member = Member::load( $val, 'name' );
				if( !$item->canBeAssigned( $member ) )
				{
					throw new DomainException( 'form_err_bad_assignment' );
				}
			}

			return true;
		} );
	}
}