<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\Statistics;

use JobQueueGroup;
use MediaWiki\Config\Config;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
use MediaWiki\Extension\Translate\TranslatorInterface\EntitySearch;
use MediaWiki\Html\Html;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\SpecialPage\SpecialPage;
use MessagePrefixMessageGroup;

/**
 * Implements includable special page Special:MessageGroupStats which provides
 * translation statistics for all languages for a group.
 *
 * @author Niklas Laxström
 * @author Siebrand Mazeland
 * @license GPL-2.0-or-later
 * @ingroup SpecialPage TranslateSpecialPage Stats
 */
class MessageGroupStatsSpecialPage extends SpecialPage {
	/** Whether to hide rows which are fully translated. */
	private bool $noComplete = true;
	/** Whether to hide rows which are fully untranslated. */
	private bool $noEmpty = false;
	/** The target of stats: group id or message prefix. */
	private string $target;
	/** The target type of stats requested: */
	private ?string $targetType = null;
	private ServiceOptions $options;
	private JobQueueGroup $jobQueueGroup;
	private MessageGroupStatsTableFactory $messageGroupStatsTableFactory;
	private EntitySearch $entitySearch;
	private MessagePrefixStats $messagePrefixStats;
	private LanguageNameUtils $languageNameUtils;
	private MessageGroupMetadata $messageGroupMetadata;

	private const GROUPS = 'group';
	private const MESSAGES = 'messages';

	private const CONSTRUCTOR_OPTIONS = [
		'TranslateMessagePrefixStatsLimit',
	];

	public function __construct(
		Config $config,
		JobQueueGroup $jobQueueGroup,
		MessageGroupStatsTableFactory $messageGroupStatsTableFactory,
		EntitySearch $entitySearch,
		MessagePrefixStats $messagePrefixStats,
		LanguageNameUtils $languageNameUtils,
		MessageGroupMetadata $messageGroupMetadata
	) {
		parent::__construct( 'MessageGroupStats' );
		$this->options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config );
		$this->jobQueueGroup = $jobQueueGroup;
		$this->messageGroupStatsTableFactory = $messageGroupStatsTableFactory;
		$this->entitySearch = $entitySearch;
		$this->messagePrefixStats = $messagePrefixStats;
		$this->languageNameUtils = $languageNameUtils;
		$this->messageGroupMetadata = $messageGroupMetadata;
	}

	public function getDescription() {
		return $this->msg( 'translate-mgs-pagename' );
	}

	public function isIncludable() {
		return true;
	}

	protected function getGroupName() {
		return 'translation';
	}

	public function execute( $par ) {
		$request = $this->getRequest();

		$purge = $request->getVal( 'action' ) === 'purge';
		if ( $purge && !$request->wasPosted() ) {
			LanguageStatsSpecialPage::showPurgeForm( $this->getContext() );
			return;
		}

		$this->setHeaders();
		$this->outputHeader();

		$out = $this->getOutput();

		$out->addModules( 'ext.translate.special.languagestats' );
		$out->addModuleStyles( 'ext.translate.statstable' );
		$out->addModuleStyles( 'ext.translate.special.groupstats' );

		$params = $par ? explode( '/', $par ) : [];

		if ( isset( $params[0] ) && trim( $params[0] ) ) {
			$this->target = $params[0];
		}

		if ( isset( $params[1] ) ) {
			$this->noComplete = (bool)$params[1];
		}

		if ( isset( $params[2] ) ) {
			$this->noEmpty = (bool)$params[2];
		}

		// Whether the form has been submitted, only relevant if not including
		$submitted = !$this->including() && $request->getVal( 'x' ) === 'D';

		$this->target = $request->getVal( self::GROUPS, $this->target ?? '' );
		if ( $this->target !== '' ) {
			$this->targetType = self::GROUPS;
		} else {
			$this->target = $request->getVal( self::MESSAGES, '' );
			if ( $this->target !== '' ) {
				$this->targetType = self::MESSAGES;
			}
		}

		// Default booleans to false if the form was submitted
		$this->noComplete = $request->getBool(
			'suppresscomplete',
			$this->noComplete && !$submitted
		);
		$this->noEmpty = $request->getBool( 'suppressempty', $this->noEmpty && !$submitted );

		if ( !$this->including() ) {
			$out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting' );
			$this->addForm();
		}

		$stats = $output = null;
		if ( $this->targetType === self::GROUPS && $this->isValidGroup( $this->target ) ) {
			$this->outputIntroduction();

			$stats = $this->loadStatistics( $this->target, MessageGroupStats::FLAG_CACHE_ONLY );

			$messageGroupStatsTable = $this->messageGroupStatsTableFactory->newFromContext( $this->getContext() );
			$output = $messageGroupStatsTable->get(
				$stats,
				MessageGroups::getGroup( $this->target ),
				$this->noComplete,
				$this->noEmpty
			);

			$incomplete = $messageGroupStatsTable->areStatsIncomplete();
			if ( $incomplete ) {
				$out->wrapWikiMsg(
					"<div class='error'>$1</div>",
					'translate-langstats-incomplete'
				);
			}

			if ( $incomplete || $purge ) {
				DeferredUpdates::addCallableUpdate( function () use ( $purge ) {
					// Attempt to recache on the fly the missing stats, unless a
					// purge was requested, because that is likely to time out.
					// Even though this is executed inside a deferred update, it
					// counts towards the maximum execution time limit. If that is
					// reached, or any other failure happens, no updates at all
					// will be written into the database, as it does only single
					// update at the end. Hence we always add a job too, so that
					// even the slower updates will get done at some point. In
					// regular case (no purge), the job sees that the stats are
					// already updated, so it is not much of an overhead.
					$jobParams = $this->getCacheRebuildJobParameters( $this->target );
					$jobParams[ 'purge' ] = $purge;
					$job = RebuildMessageGroupStatsJob::newJob( $jobParams );
					$this->jobQueueGroup->push( $job );

					// $purge is only true if request was posted
					if ( !$purge ) {
						$this->loadStatistics( $this->target );
					}
				} );
			}
		} elseif ( $this->targetType === self::MESSAGES ) {
			$messagesWithPrefix = $this->entitySearch->matchMessages( $this->target );
			if ( $messagesWithPrefix ) {
				$messageWithPrefixLimit = $this->options->get( 'TranslateMessagePrefixStatsLimit' );
				if ( count( $messagesWithPrefix ) > $messageWithPrefixLimit ) {
					$out->addHTML(
						Html::errorBox(
							$this->msg( 'translate-mgs-message-prefix-limit' )
								->params( $messageWithPrefixLimit )
								->parse()
						)
					);
					return;
				}

				$stats = $this->messagePrefixStats->forAll( ...$messagesWithPrefix );
				$messageGroupStatsTable = $this->messageGroupStatsTableFactory
					->newFromContext( $this->getContext() );
				$output = $messageGroupStatsTable->get(
					$stats,
					new MessagePrefixMessageGroup(),
					$this->noComplete,
					$this->noEmpty
				);
			}
		}

		if ( $output ) {
			// If output is present, put it on the page
			$out->addHTML( $output );
		} elseif ( $stats !== null ) {
			// Output not present, but stats are present. Probably an issue?
			$out->addHTML( Html::warningBox( $this->msg( 'translate-mgs-nothing' )->parse() ) );
		} elseif ( $submitted ) {
			$this->invalidTarget();
		}
	}

	private function loadStatistics( string $target, int $flags = 0 ): array {
		return MessageGroupStats::forGroup( $target, $flags );
	}

	private function getCacheRebuildJobParameters( string $target ): array {
		return [ 'groupid' => $target ];
	}

	private function isValidGroup( ?string $value ): bool {
		if ( $value === null ) {
			return false;
		}

		$group = MessageGroups::getGroup( $value );
		if ( $group ) {
			if ( MessageGroups::isDynamic( $group ) ) {
				/* Dynamic groups are not listed, but it is possible to end up
				 * on this page with a dynamic group by navigating from
				 * translation or proofreading activity or by giving group id
				 * of dynamic group explicitly. Ignore dynamic group to avoid
				 * throwing exceptions later. */
				$group = false;
			} else {
				$this->target = $group->getId();
			}
		}

		return (bool)$group;
	}

	private function invalidTarget(): void {
		$this->getOutput()->wrapWikiMsg(
			"<div class='error'>$1</div>",
			[ 'translate-mgs-invalid-group', $this->target ]
		);
	}

	private function outputIntroduction(): void {
		$priorityLangs = $this->messageGroupMetadata->get( $this->target, 'prioritylangs' );
		if ( $priorityLangs ) {
			$languagesFormatted = $this->formatLanguageList( explode( ',', $priorityLangs ) );
			$hasPriorityForce = $this->messageGroupMetadata->get( $this->target, 'priorityforce' ) === 'on';
			if ( $hasPriorityForce ) {
				$this->getOutput()->addWikiMsg( 'tpt-priority-languages-force', $languagesFormatted );
			} else {
				$this->getOutput()->addWikiMsg( 'tpt-priority-languages', $languagesFormatted );
			}
		}
	}

	private function formatLanguageList( array $codes ): string {
		foreach ( $codes as &$value ) {
			$value = $this->languageNameUtils->getLanguageName( $value, $this->getLanguage()->getCode() )
				. $this->msg( 'word-separator' )->plain()
				. $this->msg( 'parentheses', $value )->plain();
		}

		return $this->getLanguage()->listToText( $codes );
	}

	private function addForm(): void {
		$formDescriptor = [
			'select' => [
				'type' => 'select',
				'name' => self::GROUPS,
				'id' => self::GROUPS,
				'label' => $this->msg( 'translate-mgs-group' )->text(),
				'options' => $this->getGroupOptions(),
				'default' => $this->targetType === self::GROUPS ? $this->target : null,
				'cssclass' => 'message-group-selector'
			],
			'input' => [
				'type' => 'text',
				'name' => self::MESSAGES,
				'id' => self::MESSAGES,
				'label' => $this->msg( 'translate-mgs-prefix' )->text(),
				'default' => $this->targetType === self::MESSAGES ? $this->target : null,
				'cssclass' => 'message-prefix-selector'
			],
			'nocomplete-check' => [
				'type' => 'check',
				'name' => 'suppresscomplete',
				'id' => 'suppresscomplete',
				'label' => $this->msg( 'translate-mgs-nocomplete' )->text(),
				'default' => $this->noComplete,
			],
			'noempty-check' => [
				'type' => 'check',
				'name' => 'suppressempty',
				'id' => 'suppressempty',
				'label' => $this->msg( 'translate-mgs-noempty' )->text(),
				'default' => $this->noEmpty,
			]
		];

		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );

		/* Since these pages are in the tabgroup with Special:Translate,
		 * it makes sense to retain the selected group/language parameter
		 * on post requests even when not relevant to the current page. */
		$val = $this->getRequest()->getVal( 'language' );
		if ( $val !== null ) {
			$htmlForm->addHiddenField( 'language', $val );
		}

		$htmlForm
			->addHiddenField( 'x', 'D' ) // To detect submission
			->setMethod( 'get' )
			->setId( 'mw-message-group-stats-form' )
			->setSubmitTextMsg( 'translate-mgs-submit' )
			->setWrapperLegendMsg( 'translate-mgs-fieldset' )
			->prepareForm()
			->displayForm( false );
	}

	/** Creates a simple message group options. */
	private function getGroupOptions(): array {
		$options = [ '' => null ];
		$groups = MessageGroups::getAllGroups();

		foreach ( $groups as $id => $class ) {
			if ( MessageGroups::getGroup( $id )->exists() ) {
				$options[$class->getLabel()] = $id;
			}
		}

		return $options;
	}
}
