<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\PageTranslation;

use ContentHandler;
use DifferenceEngine;
use ErrorPageError;
use IDBAccessObject;
use InvalidArgumentException;
use JobQueueGroup;
use ManualLogEntry;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleState;
use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
use MediaWiki\Extension\Translate\Statistics\RebuildMessageGroupStatsJob;
use MediaWiki\Extension\Translate\Synchronization\MessageWebImporter;
use MediaWiki\Extension\Translate\Utilities\LanguagesMultiselectWidget;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\Extension\TranslationNotifications\SpecialNotifyTranslators;
use MediaWiki\Html\Html;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Page\PageRecord;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Request\WebRequest;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use OOUI\ButtonInputWidget;
use OOUI\CheckboxInputWidget;
use OOUI\DropdownInputWidget;
use OOUI\FieldLayout;
use OOUI\FieldsetLayout;
use OOUI\HtmlSnippet;
use OOUI\RadioInputWidget;
use OOUI\TextInputWidget;
use PermissionsError;
use UnexpectedValueException;
use UserBlockedError;
use Wikimedia\Rdbms\IResultWrapper;
use Xml;
use function count;
use function wfEscapeWikiText;

/**
 * A special page for marking revisions of pages for translation.
 *
 * This page is the main tool for translation administrators in the wiki.
 * It will list all pages in their various states and provides actions
 * that are suitable for given translatable page.
 *
 * @author Niklas Laxström
 * @author Siebrand Mazeland
 * @license GPL-2.0-or-later
 */
class PageTranslationSpecialPage extends SpecialPage {
	private const DISPLAY_STATUS_MAPPING = [
		TranslatablePageStatus::PROPOSED => 'proposed',
		TranslatablePageStatus::ACTIVE => 'active',
		TranslatablePageStatus::OUTDATED => 'outdated',
		TranslatablePageStatus::BROKEN => 'broken'
	];
	private LanguageFactory $languageFactory;
	private LinkBatchFactory $linkBatchFactory;
	private JobQueueGroup $jobQueueGroup;
	private PermissionManager $permissionManager;
	private TranslatablePageMarker $translatablePageMarker;
	private TranslatablePageParser $translatablePageParser;
	private MessageGroupMetadata $messageGroupMetadata;
	private TranslatablePageView $translatablePageView;
	private TranslatablePageStateStore $translatablePageStateStore;

	public function __construct(
		LanguageFactory $languageFactory,
		LinkBatchFactory $linkBatchFactory,
		JobQueueGroup $jobQueueGroup,
		PermissionManager $permissionManager,
		TranslatablePageMarker $translatablePageMarker,
		TranslatablePageParser $translatablePageParser,
		MessageGroupMetadata $messageGroupMetadata,
		TranslatablePageView $translatablePageView,
		TranslatablePageStateStore $translatablePageStateStore
	) {
		parent::__construct( 'PageTranslation' );
		$this->languageFactory = $languageFactory;
		$this->linkBatchFactory = $linkBatchFactory;
		$this->jobQueueGroup = $jobQueueGroup;
		$this->permissionManager = $permissionManager;
		$this->translatablePageMarker = $translatablePageMarker;
		$this->translatablePageParser = $translatablePageParser;
		$this->messageGroupMetadata = $messageGroupMetadata;
		$this->translatablePageView = $translatablePageView;
		$this->translatablePageStateStore = $translatablePageStateStore;
	}

	public function doesWrites(): bool {
		return true;
	}

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

	public function execute( $parameters ) {
		$this->setHeaders();

		$user = $this->getUser();
		$request = $this->getRequest();

		$target = $request->getText( 'target', $parameters ?? '' );
		$revision = $request->getIntOrNull( 'revision' );
		$action = $request->getVal( 'do' );
		$out = $this->getOutput();
		$out->addModules( 'ext.translate.special.pagetranslation' );
		$out->addModuleStyles( 'ext.translate.specialpages.styles' );
		$out->addHelpLink( 'Help:Extension:Translate/Page_translation_example' );
		$out->enableOOUI();

		if ( $target === '' ) {
			$this->listPages();

			return;
		}

		$title = Title::newFromText( $target );
		if ( !$title ) {
			$out->wrapWikiMsg( Html::errorBox( '$1' ), [ 'tpt-badtitle', $target ] );
			$out->addWikiMsg( 'tpt-list-pages-in-translations' );

			return;
		}

		$this->getSkin()->setRelevantTitle( $title );

		if ( !$title->exists() ) {
			$out->wrapWikiMsg(
				Html::errorBox( '$1' ),
				[ 'tpt-nosuchpage', $title->getPrefixedText() ]
			);
			$out->addWikiMsg( 'tpt-list-pages-in-translations' );

			return;
		}

		if ( $action === 'settings' && !$this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
			$this->showTranslationStateRestricted();
			return;
		}

		$block = $this->getBlock( $request, $user, $title );
		if ( $action === 'settings' && !$request->wasPosted() ) {
			$this->showTranslationSettings( $title, $block );
			return;
		}

		if ( $block ) {
			throw $block;
		}

		// Check token for all POST actions here
		$csrfTokenSet = $this->getContext()->getCsrfTokenSet();
		if ( $request->wasPosted() && !$csrfTokenSet->matchTokenField( 'token' ) ) {
			throw new PermissionsError( 'pagetranslation' );
		}

		if ( $action === 'settings' && $request->wasPosted() ) {
			$this->handleTranslationState( $title, $request->getRawVal( 'translatable-page-state' ) );
			return;
		}

		// Anything other than listing the pages or manipulating settings needs permissions
		if ( !$user->isAllowed( 'pagetranslation' ) ) {
			throw new PermissionsError( 'pagetranslation' );
		}

		if ( $action === 'mark' ) {
			// Has separate form
			$this->onActionMark( $title, $revision );

			return;
		}

		// On GET requests, show form which has token
		if ( !$request->wasPosted() ) {
			if ( $action === 'unlink' ) {
				$this->showUnlinkConfirmation( $title );
			} else {
				$params = [
					'do' => $action,
					'target' => $title->getPrefixedText(),
					'revision' => $revision,
				];
				$this->showGenericConfirmation( $params );
			}

			return;
		}

		if ( $action === 'discourage' || $action === 'encourage' ) {
			$id = TranslatablePage::getMessageGroupIdFromTitle( $title );
			$current = MessageGroups::getPriority( $id );

			if ( $action === 'encourage' ) {
				$new = '';
			} else {
				$new = 'discouraged';
			}

			if ( $new !== $current ) {
				MessageGroups::setPriority( $id, $new );
				$entry = new ManualLogEntry( 'pagetranslation', $action );
				$entry->setPerformer( $user );
				$entry->setTarget( $title );
				$logId = $entry->insert();
				$entry->publish( $logId );
			}

			// Defer stats purging of parent aggregate groups. Shared groups can contain other
			// groups as well, which we do not need to update. We could filter non-aggregate
			// groups out, or use MessageGroups::getParentGroups, though it has an inconvenient
			// return value format for this use case.
			$group = MessageGroups::getGroup( $id );
			$sharedGroupIds = MessageGroups::getSharedGroups( $group );
			if ( $sharedGroupIds !== [] ) {
				$job = RebuildMessageGroupStatsJob::newRefreshGroupsJob( $sharedGroupIds );
				$this->jobQueueGroup->push( $job );
			}

			// Show updated page with a notice
			$this->listPages();

			return;
		}

		if ( $action === 'unlink' || $action === 'unmark' ) {
			try {
				$this->translatablePageMarker->unmarkPage(
					TranslatablePage::newFromTitle( $title ),
					$user,
					$action === 'unlink'
				);

				$out->wrapWikiMsg(
					Html::successBox( '$1' ),
					[ 'tpt-unmarked', $title->getPrefixedText() ]
				);
			} catch ( TranslatablePageMarkException $e ) {
				$out->wrapWikiMsg(
					Html::errorBox( '$1' ),
					$e->getMessageObject()
				);
			}

			$out->addWikiMsg( 'tpt-list-pages-in-translations' );
		}
	}

	protected function onActionMark( Title $title, ?int $revision ): void {
		$request = $this->getRequest();
		$out = $this->getOutput();
		$translateTitle = $request->getCheck( 'translatetitle' );

		try {
			$operation = $this->translatablePageMarker->getMarkOperation(
				$title->toPageRecord(
					$request->wasPosted() ? IDBAccessObject::READ_LATEST : IDBAccessObject::READ_NORMAL
				),
				$revision,
				// If the request was not posted, validate all the units so that initially we display all the errors
				// and then the user can choose whether they want to translate the title
				!$request->wasPosted() || $translateTitle
			);
		} catch ( TranslatablePageMarkException $e ) {
			$out->addHTML( Html::errorBox( $this->msg( $e->getMessageObject() )->parse() ) );
			$out->addWikiMsg( 'tpt-list-pages-in-translations' );
			return;
		}

		$unitNameValidationResult = $operation->getUnitValidationStatus();
		// Non-fatal error which prevents saving
		if ( $unitNameValidationResult->isOK() && $request->wasPosted() ) {
			// Fetch priority language related information
			[ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ] =
				$this->getPriorityLanguage( $this->getRequest() );

			$unitFuzzySelector = $request->getRawVal( 'unit-fuzzy-selector' );
			if ( $unitFuzzySelector === 'all' ) {
				$noFuzzyUnits = [];
			} else {
				// Get IDs of all changed units
				$allChangedUnits = array_map(
					static fn ( $unit ) => $unit->id,
					array_filter(
						$operation->getUnits(),
						static fn ( $unit ) => $unit->type === 'changed'
					)
				);

				if ( $unitFuzzySelector === 'none' ) {
					$noFuzzyUnits = $allChangedUnits;
				} else { // custom
					$fuzzyUnits = $request->getArray( 'tpt-sect-fuzzy' ) ?? [];
					// Filter the units that should not be fuzzied
					$noFuzzyUnits = array_filter(
						$allChangedUnits,
						static fn ( $value ) => !in_array( $value, $fuzzyUnits )
					);
				}
			}

			$translatablePageSettings = new TranslatablePageSettings(
				$priorityLanguages,
				$forcePriorityLanguage,
				$priorityLanguageReason,
				$noFuzzyUnits,
				$translateTitle,
				$request->getCheck( 'use-latest-syntax' ),
				$request->getCheck( 'transclusion' )
			);

			try {
				$unitCount = $this->translatablePageMarker->markForTranslation(
					$operation,
					$translatablePageSettings,
					$this->getUser()
				);
				$this->showSuccess( $operation->getPage(), $operation->isFirstMark(), $unitCount );
			} catch ( TranslatablePageMarkException $e ) {
				$out->wrapWikiMsg(
					Html::errorBox( '$1' ),
					$e->getMessageObject()
				);
			}
		} else {
			if ( !$unitNameValidationResult->isOK() ) {
				$out->addHTML(
					Html::errorBox(
						$unitNameValidationResult->getHTML( false, false, $this->getLanguage() )
					)
				);
			}

			$this->showPage( $operation );
		}
	}

	/**
	 * Displays success message and other instructions after a page has been marked for translation.
	 * @param TranslatablePage $page
	 * @param bool $firstMark true if it is the first time the page is being marked for translation.
	 * @param int $unitCount
	 * @return void
	 */
	private function showSuccess( TranslatablePage $page, bool $firstMark, int $unitCount ): void {
		$titleText = $page->getTitle()->getPrefixedText();
		$num = $this->getLanguage()->formatNum( $unitCount );
		$link = SpecialPage::getTitleFor( 'Translate' )->getFullURL( [
			'group' => $page->getMessageGroupId(),
			'action' => 'page',
			'filter' => '',
		] );

		$this->getOutput()->wrapWikiMsg(
			Html::successBox( '$1' ),
			[ 'tpt-saveok', $titleText, $num, $link ]
		);

		// If the page is being marked for translation for the first time
		// add a link to Special:PageMigration.
		if ( $firstMark ) {
			$this->getOutput()->addWikiMsg( 'tpt-saveok-first' );
		}

		// If TranslationNotifications is installed, and the user can notify
		// translators, add a convenience link.
		if ( method_exists( SpecialNotifyTranslators::class, 'execute' ) &&
			$this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
		) {
			$link = SpecialPage::getTitleFor( 'NotifyTranslators' )->getFullURL(
				[ 'tpage' => $page->getTitle()->getArticleID() ]
			);
			$this->getOutput()->addWikiMsg( 'tpt-offer-notify', $link );
		}

		$this->getOutput()->addWikiMsg( 'tpt-list-pages-in-translations' );
	}

	private function showGenericConfirmation( array $params ): void {
		$formParams = [
			'method' => 'post',
			'action' => $this->getPageTitle()->getLocalURL(),
		];

		$params['title'] = $this->getPageTitle()->getPrefixedText();
		$params['token'] = $this->getContext()->getCsrfTokenSet()->getToken();

		$hidden = '';
		foreach ( $params as $key => $value ) {
			$hidden .= Html::hidden( $key, $value );
		}

		$this->getOutput()->addHTML(
			Html::openElement( 'form', $formParams ) .
			$hidden .
			$this->msg( 'tpt-generic-confirm' )->parseAsBlock() .
			Html::submitButton(
				$this->msg( 'tpt-generic-button' )->text(),
				[ 'class' => 'mw-ui-button mw-ui-progressive' ]
			) .
			Html::closeElement( 'form' )
		);
	}

	private function showUnlinkConfirmation( Title $target ): void {
		$formParams = [
			'method' => 'post',
			'action' => $this->getPageTitle()->getLocalURL(),
		];

		$this->getOutput()->addHTML(
			Html::openElement( 'form', $formParams ) .
			Html::hidden( 'do', 'unlink' ) .
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
			Html::hidden( 'target', $target->getPrefixedText() ) .
			Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
			$this->msg( 'tpt-unlink-confirm', $target->getPrefixedText() )->parseAsBlock() .
			Html::submitButton(
				$this->msg( 'tpt-unlink-button' )->text(),
				[ 'class' => 'mw-ui-button mw-ui-destructive' ]
			) .
			Html::closeElement( 'form' )
		);
	}

	/**
	 * TODO: Move this function to SyncTranslatableBundleStatusMaintenanceScript once we
	 * start using the translatable_bundles table for fetching the translatabale pages
	 */
	public static function loadPagesFromDB(): IResultWrapper {
		$dbr = Utilities::getSafeReadDB();
		return $dbr->newSelectQueryBuilder()
			->select( [
				'page_id',
				'page_namespace',
				'page_title',
				'page_latest',
				'rt_revision' => 'MAX(rt_revision)',
				'rt_type'
			] )
			->from( 'page' )
			->join( 'revtag', null, 'page_id=rt_page' )
			->where( [
				'rt_type' => [ RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ],
			] )
			->orderBy( [ 'page_namespace', 'page_title' ] )
			->groupBy( [ 'page_id', 'page_namespace', 'page_title', 'page_latest', 'rt_type' ] )
			->caller( __METHOD__ )
			->fetchResultSet();
	}

	/**
	 * TODO: Move this function to SyncTranslatableBundleStatusMaintenanceScript once we
	 * start using the translatable_bundles table for fetching the translatabale pages
	 */
	public static function buildPageArray( IResultWrapper $res ): array {
		$pages = [];
		foreach ( $res as $r ) {
			// We have multiple rows for same page, because of different tags
			if ( !isset( $pages[$r->page_id] ) ) {
				$pages[$r->page_id] = [];
				$title = Title::newFromRow( $r );
				$pages[$r->page_id]['title'] = $title;
				$pages[$r->page_id]['latest'] = (int)$title->getLatestRevID();
			}

			$tag = $r->rt_type;
			$pages[$r->page_id][$tag] = (int)$r->rt_revision;
		}

		return $pages;
	}

	/**
	 * Classify a list of pages and amend them with additional metadata.
	 * @param array[] $pages
	 * @return array[]
	 * @phan-return array{proposed:array[],active:array[],broken:array[],outdated:array[]}
	 */
	private function classifyPages( array $pages ): array {
		$out = [
			// The ideal state for pages: marked and up to date
			'active' => [],
			'proposed' => [],
			'outdated' => [],
			'broken' => [],
		];

		if ( $pages === [] ) {
			return $out;
		}

		// Preload stuff for performance
		$messageGroupIdsForPreload = [];
		foreach ( $pages as $i => $page ) {
			$id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] );
			$messageGroupIdsForPreload[] = $id;
			$pages[$i]['groupid'] = $id;
		}
		// Performance optimization: load only data we need to classify the pages
		$metadata = $this->messageGroupMetadata->loadBasicMetadataForTranslatablePages(
			$messageGroupIdsForPreload,
			[ 'transclusion', 'version' ]
		);

		foreach ( $pages as $page ) {
			$groupId = $page['groupid'];
			$group = MessageGroups::getGroup( $groupId );
			$page['discouraged'] = MessageGroups::getPriority( $group ) === 'discouraged';
			$page['version'] = $metadata[$groupId]['version'] ?? TranslatablePageMarker::DEFAULT_SYNTAX_VERSION;
			$page['transclusion'] = $metadata[$groupId]['transclusion'] ?? false;

			// TODO: Eventually we should query the status directly from the TranslatableBundleStore
			$tpStatus = TranslatablePage::determineStatus(
				$page[RevTagStore::TP_READY_TAG] ?? null,
				$page[RevTagStore::TP_MARK_TAG] ?? null,
				$page['latest']
			);

			if ( !$tpStatus ) {
				// Ignore pages for which status could not be determined.
				continue;
			}

			$out[self::DISPLAY_STATUS_MAPPING[$tpStatus->getId()]][] = $page;
		}

		return $out;
	}

	public function listPages(): void {
		$out = $this->getOutput();

		$res = self::loadPagesFromDB();
		$allPages = self::buildPageArray( $res );

		$pagesWithProposedState = [];
		if ( $this->translatablePageView->isTranslationBannerNamespaceConfigured() ) {
			$pagesWithProposedState = $this->translatablePageStateStore->getRequested();
		}

		if ( !count( $allPages ) && !count( $pagesWithProposedState ) ) {
			$out->addWikiMsg( 'tpt-list-nopages' );

			return;
		}

		$lb = $this->linkBatchFactory->newLinkBatch();
		$lb->setCaller( __METHOD__ );
		foreach ( $allPages as $page ) {
			$lb->addObj( $page['title'] );
		}

		foreach ( $pagesWithProposedState as $title ) {
			$lb->addObj( $title );
		}
		$lb->execute();

		$types = $this->classifyPages( $allPages );

		$pages = $types['proposed'];
		if ( $pages || $pagesWithProposedState ) {
			$out->wrapWikiMsg( '== $1 ==', 'tpt-new-pages-title' );
			if ( $pages ) {
				$out->addWikiMsg( 'tpt-new-pages', count( $pages ) );
				$out->addHTML( $this->getPageList( $pages, 'proposed' ) );
			}

			if ( $pagesWithProposedState ) {
				$out->addWikiMsg( 'tpt-proposed-state-pages', count( $pagesWithProposedState ) );
				$out->addHTML( $this->displayPagesWithProposedState( $pagesWithProposedState ) );
			}
		}

		$pages = $types['broken'];
		if ( $pages ) {
			$out->wrapWikiMsg( '== $1 ==', 'tpt-other-pages-title' );
			$out->addWikiMsg( 'tpt-other-pages', count( $pages ) );
			$out->addHTML( $this->getPageList( $pages, 'broken' ) );
		}

		$pages = $types['outdated'];
		if ( $pages ) {
			$out->wrapWikiMsg( '== $1 ==', 'tpt-outdated-pages-title' );
			$out->addWikiMsg( 'tpt-outdated-pages', count( $pages ) );
			$out->addHTML( $this->getPageList( $pages, 'outdated' ) );
		}

		$pages = $types['active'];
		if ( $pages ) {
			$out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' );
			$out->addWikiMsg( 'tpt-old-pages', count( $pages ) );
			$out->addHTML( $this->getPageList( $pages, 'active' ) );
		}
	}

	private function actionLinks( array $page, string $type ): string {
		// Performance optimization to avoid calling $this->msg in a loop
		static $messageCache = null;
		if ( $messageCache === null ) {
			$messageCache = [
				'mark' => $this->msg( 'tpt-rev-mark' )->text(),
				'mark-tooltip' => $this->msg( 'tpt-rev-mark-tooltip' )->text(),
				'encourage' => $this->msg( 'tpt-rev-encourage' )->text(),
				'encourage-tooltip' => $this->msg( 'tpt-rev-encourage-tooltip' )->text(),
				'discourage' => $this->msg( 'tpt-rev-discourage' )->text(),
				'discourage-tooltip' => $this->msg( 'tpt-rev-discourage-tooltip' )->text(),
				'unmark' => $this->msg( 'tpt-rev-unmark' )->text(),
				'unmark-tooltip' => $this->msg( 'tpt-rev-unmark-tooltip' )->text(),
				'pipe-separator' => $this->msg( 'pipe-separator' )->escaped(),
			];
		}

		$actions = [];
		/** @var Title $title */
		$title = $page['title'];
		$user = $this->getUser();

		// Class to allow one-click POSTs
		$js = [ 'class' => 'mw-translate-jspost' ];

		if ( $user->isAllowed( 'pagetranslation' ) ) {
			// Enable re-marking of all pages to allow changing of priority languages
			// or migration to the new syntax version
			if ( $type !== 'broken' ) {
				$actions[] = $this->getLinkRenderer()->makeKnownLink(
					$this->getPageTitle(),
					$messageCache['mark'],
					[ 'title' => $messageCache['mark-tooltip'] ],
					[
						'do' => 'mark',
						'target' => $title->getPrefixedText(),
						'revision' => $title->getLatestRevID(),
					]
				);
			}

			if ( $type !== 'proposed' ) {
				if ( $page['discouraged'] ) {
					$actions[] = $this->getLinkRenderer()->makeKnownLink(
						$this->getPageTitle(),
						$messageCache['encourage'],
						[ 'title' => $messageCache['encourage-tooltip'] ] + $js,
						[
							'do' => 'encourage',
							'target' => $title->getPrefixedText(),
							'revision' => -1,
						]
					);
				} else {
					$actions[] = $this->getLinkRenderer()->makeKnownLink(
						$this->getPageTitle(),
						$messageCache['discourage'],
						[ 'title' => $messageCache['discourage-tooltip'] ] + $js,
						[
							'do' => 'discourage',
							'target' => $title->getPrefixedText(),
							'revision' => -1,
						]
					);
				}

				$actions[] = $this->getLinkRenderer()->makeKnownLink(
					$this->getPageTitle(),
					$messageCache['unmark'],
					[ 'title' => $messageCache['unmark-tooltip'] ],
					[
						'do' => $type === 'broken' ? 'unmark' : 'unlink',
						'target' => $title->getPrefixedText(),
						'revision' => -1,
					]
				);
			}
		}

		if ( !$actions ) {
			return '';
		}

		return '<div>' . implode( $messageCache['pipe-separator'], $actions ) . '</div>';
	}

	private function showPage( TranslatablePageMarkOperation $operation ): void {
		$page = $operation->getPage();
		$out = $this->getOutput();
		$out->addWikiMsg( 'tpt-showpage-intro' );

		$this->addPageForm(
			$page->getTitle(),
			'mw-tpt-sp-markform',
			'mark',
			$page->getRevision()
		);

		$out->wrapWikiMsg( '==$1==', 'tpt-sections-oldnew' );

		$diffOld = $this->msg( 'tpt-diff-old' )->escaped();
		$diffNew = $this->msg( 'tpt-diff-new' )->escaped();
		$hasChanges = false;

		// Check whether page title was previously marked for translation.
		// If the page is marked for translation the first time, default to checked,
		// unless the page is a template. T305240
		$defaultChecked = (
			$operation->isFirstMark() &&
			!$page->getTitle()->inNamespace( NS_TEMPLATE )
		) || $page->hasPageDisplayTitle();

		$sourceLanguage = $this->languageFactory->getLanguage( $page->getSourceLanguageCode() );

		// Check if there are changed units
		if ( array_filter(
			$operation->getUnits(),
			static fn ( $unit ) => $unit->type === 'changed'
		) ) {
			// General Area
			$dropdown = new FieldLayout(
				new DropdownInputWidget( [
					'name' => 'unit-fuzzy-selector',
					'options' => [
						[
							'data' => 'all',
							'label' => $this->msg( 'tpt-fuzzy-select-all' )->text()
						],
						[
							'data' => 'none',
							'label' => $this->msg( 'tpt-fuzzy-select-none' )->text()
						],
						[
							'data' => 'custom',
							'label' => $this->msg( 'tpt-fuzzy-select-custom' )->text()
						]
					],
					'value' => 'custom'
				] ),
				[
					'label' => $this->msg( 'tpt-fuzzy-select-label' )->text(),
					'align' => 'left',
				]
			);
			$out->addHTML( MessageWebImporter::makeSectionElement(
				$this->msg( 'tpt-general-area-header' )->text(),
				'dropdown',
				$dropdown->toString()
			) );
		}

		foreach ( $operation->getUnits() as $s ) {
			if ( $s->id === TranslatablePage::DISPLAY_TITLE_UNIT_ID ) {
				// Set section type as new if title previously unchecked
				$s->type = $defaultChecked ? $s->type : 'new';

				// Checkbox for page title optional translation
				$checkBox = new FieldLayout(
					new CheckboxInputWidget( [
						'name' => 'translatetitle',
						'selected' => $defaultChecked,
					] ),
					[
						'label' => $this->msg( 'tpt-translate-title' )->text(),
						'align' => 'inline',
						'classes' => [ 'mw-tpt-m-vertical' ]
					]
				);
				$out->addHTML( $checkBox->toString() );
			}

			if ( $s->type === 'new' ) {
				$hasChanges = true;
				$name = $this->msg( 'tpt-section-new', $s->id )->escaped();
			} else {
				$name = $this->msg( 'tpt-section', $s->id )->escaped();
			}

			if ( $s->type === 'changed' ) {
				$hasChanges = true;
				$diff = new DifferenceEngine();
				$diff->setTextLanguage( $sourceLanguage );
				$diff->setReducedLineNumbers();

				$oldContent = ContentHandler::makeContent( $s->getOldText(), $diff->getTitle() );
				$newContent = ContentHandler::makeContent( $s->getText(), $diff->getTitle() );

				$diff->setContent( $oldContent, $newContent );

				$text = $diff->getDiff( $diffOld, $diffNew );
				$diffOld = $diffNew = null;
				$diff->showDiffStyle();

				$checkLabel = new FieldLayout(
					new CheckboxInputWidget( [
						'name' => 'tpt-sect-fuzzy[]',
						'value' => $s->id,
						'selected' => !$s->onlyTvarsChanged()
					] ),
					[
						'label' => $this->msg( 'tpt-action-fuzzy' )->text(),
						'align' => 'inline',
						'classes' => [ 'mw-tpt-m-vertical', 'mw-tpt-action-field' ],
					]
				);
				$text = $checkLabel->toString() . $text;
			} else {
				$text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
			}

			# For changed text, the language is set by $diff->setTextLanguage()
			$lang = $s->type === 'changed' ? null : $sourceLanguage;
			$out->addHTML( MessageWebImporter::makeSectionElement(
				$name,
				$s->type,
				$text,
				$lang
			) );

			foreach ( $s->getIssues() as $issue ) {
				$severity = $issue->getSeverity();
				if ( $severity === TranslationUnitIssue::WARNING ) {
					$box = Html::warningBox( $this->msg( $issue )->escaped() );
				} elseif ( $severity === TranslationUnitIssue::ERROR ) {
					$box = Html::errorBox( $this->msg( $issue )->escaped() );
				} else {
					throw new UnexpectedValueException(
						"Unknown severity: $severity for key: {$issue->getKey()}"
					);
				}

				$out->addHTML( $box );
			}
		}

		if ( $operation->getDeletedUnits() ) {
			$hasChanges = true;
			$out->wrapWikiMsg( '==$1==', 'tpt-sections-deleted' );

			foreach ( $operation->getDeletedUnits() as $s ) {
				$name = $this->msg( 'tpt-section-deleted', $s->id )->escaped();
				$text = Utilities::convertWhiteSpaceToHTML( $s->getText() );
				$out->addHTML( MessageWebImporter::makeSectionElement(
					$name,
					'deleted',
					$text,
					$sourceLanguage
				) );
			}
		}

		// Display template changes if applicable
		$markedTag = $page->getMarkedTag();
		if ( $markedTag !== null ) {
			$hasChanges = true;
			$newTemplate = $operation->getParserOutput()->sourcePageTemplateForDiffs();
			$oldPage = TranslatablePage::newFromRevision(
				$page->getTitle(),
				$markedTag
			);
			$oldTemplate = $this->translatablePageParser
				->parse( $oldPage->getText() )
				->sourcePageTemplateForDiffs();

			if ( $oldTemplate !== $newTemplate ) {
				$out->wrapWikiMsg( '==$1==', 'tpt-sections-template' );

				$diff = new DifferenceEngine();
				$diff->setTextLanguage( $sourceLanguage );

				$oldContent = ContentHandler::makeContent( $oldTemplate, $diff->getTitle() );
				$newContent = ContentHandler::makeContent( $newTemplate, $diff->getTitle() );

				$diff->setContent( $oldContent, $newContent );

				$text = $diff->getDiff(
					$this->msg( 'tpt-diff-old' )->escaped(),
					$this->msg( 'tpt-diff-new' )->escaped()
				);
				$diff->showDiffStyle();
				$diff->setReducedLineNumbers();

				$out->addHTML( Xml::tags( 'div', [], $text ) );
			}
		}

		if ( !$hasChanges ) {
			$out->wrapWikiMsg( Html::successBox( '$1' ), 'tpt-mark-nochanges' );
		}

		$this->priorityLanguagesForm( $page );

		// If an existing page does not have the supportsTransclusion flag, keep the checkbox unchecked,
		// If the page is being marked for translation for the first time, the checkbox can be checked
		$this->templateTransclusionForm( $page, $page->supportsTransclusion() ?? $operation->isFirstMark() );

		$version = $this->messageGroupMetadata->getWithDefaultValue(
			$page->getMessageGroupId(), 'version', TranslatablePageMarker::DEFAULT_SYNTAX_VERSION
		);
		$this->syntaxVersionForm( $version, $operation->isFirstMark() );

		$submitButton = new FieldLayout(
			new ButtonInputWidget( [
				'label' => $this->msg( 'tpt-submit' )->text(),
				'type' => 'submit',
				'flags' => [ 'primary', 'progressive' ],
			] ),
			[
				'label' => null,
				'align' => 'top',
			]
		);

		$out->addHTML( $submitButton->toString() );
		$out->addHTML( '</form>' );
	}

	private function priorityLanguagesForm( TranslatablePage $page ): void {
		$groupId = $page->getMessageGroupId();
		$interfaceLanguage = $this->getLanguage()->getCode();
		$storedLanguages = (string)$this->messageGroupMetadata->get( $groupId, 'prioritylangs' );
		$default = $storedLanguages !== '' ? explode( ',', $storedLanguages ) : [];

		$priorityReason = $this->messageGroupMetadata->get( $groupId, 'priorityreason' );
		$priorityReason = $priorityReason !== false ? $priorityReason : '';

		$form = new FieldsetLayout( [
			'items' => [
				new FieldLayout(
					new LanguagesMultiselectWidget( [
						'infusable' => true,
						'name' => 'prioritylangs',
						'id' => 'mw-translate-SpecialPageTranslation-prioritylangs',
						'languages' => Utilities::getLanguageNames( $interfaceLanguage ),
						'default' => $default,
					] ),
					[
						'label' => $this->msg( 'tpt-select-prioritylangs' )->text(),
						'align' => 'top',
					]
				),
				new FieldLayout(
					new CheckboxInputWidget( [
						'name' => 'forcelimit',
						'selected' => $this->messageGroupMetadata->get( $groupId, 'priorityforce' ) === 'on',
					] ),
					[
						'label' => $this->msg( 'tpt-select-prioritylangs-force' )->text(),
						'align' => 'inline',
						'help' => new HtmlSnippet( $this->msg( 'tpt-select-no-prioritylangs-force' )->parse() ),
					]
				),
				new FieldLayout(
					new TextInputWidget( [
						'name' => 'priorityreason',
						'value' => $priorityReason
					] ),
					[
						'label' => $this->msg( 'tpt-select-prioritylangs-reason' )->text(),
						'align' => 'top',
					]
				),

			],
		] );

		$this->getOutput()->wrapWikiMsg( '==$1==', 'tpt-sections-prioritylangs' );
		$this->getOutput()->addHTML( $form->toString() );
	}

	private function syntaxVersionForm( string $version, bool $firstMark ): void {
		$out = $this->getOutput();

		if ( $version === TranslatablePageMarker::LATEST_SYNTAX_VERSION || $firstMark ) {
			return;
		}

		$out->wrapWikiMsg( '==$1==', 'tpt-sections-syntaxversion' );
		$out->addWikiMsg(
			'tpt-syntaxversion-text',
			'<code>' . wfEscapeWikiText( '<span lang="en" dir="ltr">...</span>' ) . '</code>',
			'<code>' . wfEscapeWikiText( '<translate nowrap>...</translate>' ) . '</code>'
		);

		$checkBox = new FieldLayout(
			new CheckboxInputWidget( [
				'name' => 'use-latest-syntax'
			] ),
			[
				'label' => $out->msg( 'tpt-syntaxversion-label' )->text(),
				'align' => 'inline',
			]
		);

		$out->addHTML( $checkBox->toString() );
	}

	private function templateTransclusionForm( TranslatablePage $page, bool $supportsTransclusion ): void {
		$out = $this->getOutput();
		$out->wrapWikiMsg( '==$1==', 'tpt-transclusion' );

		$checkBox = new FieldLayout(
			new CheckboxInputWidget( [
				'name' => 'transclusion',
				'selected' => $supportsTransclusion
			] ),
			[
				'label' => $out->msg( 'tpt-transclusion-label' )->text(),
				'align' => 'inline',
				'help' => $out->msg( 'tpt-transclusion-help' )
					->params( $page->getTitle()->getSubpage( 'de' )->getPrefixedText() )
					->text(),
				'helpInline' => true,
			]
		);

		$out->addHTML( $checkBox->toString() );
	}

	private function getPriorityLanguage( WebRequest $request ): array {
		// Get the priority languages from the request
		// We've to do some extra work here because if JS is disabled, we will be getting
		// the values split by newline.
		$priorityLanguages = rtrim( trim( $request->getVal( 'prioritylangs', '' ) ), ',' );
		$priorityLanguages = str_replace( "\n", ',', $priorityLanguages );
		$priorityLanguages = array_map( 'trim', explode( ',', $priorityLanguages ) );
		$priorityLanguages = array_unique( array_filter( $priorityLanguages ) );

		$forcePriorityLanguage = $request->getCheck( 'forcelimit' );
		$priorityLanguageReason = trim( $request->getText( 'priorityreason' ) );

		return [ $priorityLanguages, $forcePriorityLanguage, $priorityLanguageReason ];
	}

	private function getPageList( array $pages, string $type ): string {
		$items = [];
		$tagsTextCache = [];

		$tagDiscouraged = $this->msg( 'tpt-tag-discouraged' )->escaped();
		$tagOldSyntax = $this->msg( 'tpt-tag-oldsyntax' )->escaped();
		$tagNoTransclusionSupport = $this->msg( 'tpt-tag-no-transclusion-support' )->escaped();

		foreach ( $pages as $page ) {
			$link = $this->getLinkRenderer()->makeKnownLink( $page['title'] );
			$acts = $this->actionLinks( $page, $type );
			$tags = [];
			if ( $page['discouraged'] ) {
				$tags[] = $tagDiscouraged;
			}
			if ( $type !== 'proposed' ) {
				if ( $page['version'] !== TranslatablePageMarker::LATEST_SYNTAX_VERSION ) {
					$tags[] = $tagOldSyntax;
				}

				if ( $page['transclusion'] !== '1' ) {
					$tags[] = $tagNoTransclusionSupport;
				}
			}

			$tagList = '';
			if ( $tags ) {
				// Performance optimization to avoid calling $this->msg in a loop
				$tagsKey = implode( '', $tags );
				$tagsTextCache[$tagsKey] ??= $this->msg( 'parentheses' )
					->rawParams( $this->getLanguage()->pipeList( $tags ) )
					->escaped();

				$tagList = Html::rawElement(
					'span',
					[ 'class' => 'mw-tpt-actions' ],
					$tagsTextCache[$tagsKey]
				);
			}

			$items[] = "<li class='mw-tpt-pagelist-item'>$link $tagList $acts</li>";
		}

		return '<ol>' . implode( '', $items ) . '</ol>';
	}

	/** @param PageRecord[] $pagesWithProposedState */
	private function displayPagesWithProposedState( array $pagesWithProposedState ): string {
		$items = [];
		$preparePageAction = $this->msg( 'tpt-prepare-page' )->text();
		$preparePageTooltip = $this->msg( 'tpt-prepare-page-tooltip' )->text();
		$linkRenderer = $this->getLinkRenderer();
		foreach ( $pagesWithProposedState as $pageRecord ) {
			$link = $linkRenderer->makeKnownLink( $pageRecord );
			$action = $linkRenderer->makeKnownLink(
				SpecialPage::getTitleFor( 'PagePreparation' ),
				$preparePageAction,
				[ 'title' => $preparePageTooltip ],
				[ 'page' => ( Title::newFromPageReference( $pageRecord ) )->getPrefixedText() ]
			);
			$items[] = "<li class='mw-tpt-pagelist-item'>$link <div>$action</div></li>";
		}
		return '<ol>' . implode( '', $items ) . '</ol>';
	}

	private function showTranslationSettings( Title $target, ?ErrorPageError $block ): void {
		$out = $this->getOutput();
		$out->setPageTitle( $this->msg( 'tpt-translation-settings-page-title' )->text() );

		$currentState = $this->translatablePageStateStore->get( $target );

		if ( !$this->translatablePageView->canManageTranslationSettings( $target, $this->getUser() ) ) {
			$out->wrapWikiMsg( Html::errorBox( '$1' ), 'tpt-translation-settings-restricted' );
			$out->addWikiMsg( 'tpt-list-pages-in-translations' );
			return;
		}

		if ( $block ) {
			$out->wrapWikiMsg( Html::errorBox( '$1' ), $block->getMessageObject() );
		}

		if ( $currentState ) {
			$this->displayStateInfoMessage( $target, $currentState );
		}

		$this->addPageForm( $target, 'mw-tpt-sp-settings', 'settings', null );
		$out->addHTML(
			Html::rawElement(
				'p',
				[ 'class' => 'mw-tpt-vm' ],
				Html::element( 'strong', [], $this->msg( 'tpt-translation-settings-subtitle' ) )
			)
		);

		$currentStateId = $currentState ? $currentState->getStateId() : null;
		$options = new FieldsetLayout( [
			'items' => [
				new FieldLayout(
					new RadioInputWidget( [
						'name' => 'translatable-page-state',
						'value' => 'ignored',
						'selected' => $currentStateId === TranslatableBundleState::IGNORE
					] ),
					[
						'label' => $this->msg( 'tpt-translation-settings-ignore' )->text(),
						'align' => 'inline',
						'help' => $this->msg( 'tpt-translation-settings-ignore-hint' )->text(),
						'helpInline' => true,
					]
				),
				new FieldLayout(
					new RadioInputWidget( [
						'name' => 'translatable-page-state',
						'value' => 'unstable',
						'selected' => $currentStateId === null
					] ),
					[
						'label' => $this->msg( 'tpt-translation-settings-unstable' )->text(),
						'align' => 'inline',
						'help' => $this->msg( 'tpt-translation-settings-unstable-hint' )->text(),
						'helpInline' => true,
					]
				),
				new FieldLayout(
					new RadioInputWidget( [
						'name' => 'translatable-page-state',
						'value' => 'proposed',
						'selected' => $currentStateId === TranslatableBundleState::PROPOSE
					] ),
					[
						'label' => $this->msg( 'tpt-translation-settings-propose' )->text(),
						'align' => 'inline',
						'help' => $this->msg( 'tpt-translation-settings-propose-hint' )->text(),
						'helpInline' => true,
					]
				),
			],
		] );

		$out->addHTML( $options->toString() );

		$submitButton = new FieldLayout(
			new ButtonInputWidget( [
				'label' => $this->msg( 'tpt-translation-settings-save' )->text(),
				'type' => 'submit',
				'flags' => [ 'primary', 'progressive' ],
				'disabled' => $block !== null,
			] )
		);

		$out->addHTML( $submitButton->toString() );
		$out->addHTML( Html::closeElement( 'form' ) );
	}

	private function handleTranslationState( Title $title, string $selectedState ): void {
		$validStateValues = [ 'ignored', 'unstable', 'proposed' ];
		$out = $this->getOutput();
		if ( !in_array( $selectedState, $validStateValues ) ) {
			throw new InvalidArgumentException( "Invalid translation state selected: $selectedState" );
		}

		$user = $this->getUser();
		if ( !$this->translatablePageView->canManageTranslationSettings( $title, $user ) ) {
			$this->showTranslationStateRestricted();
			return;
		}

		$bundleState = TranslatableBundleState::newFromText( $selectedState );
		if ( $selectedState === 'unstable' ) {
			$this->translatablePageStateStore->remove( $title );
		} else {
			$this->translatablePageStateStore->set( $title, $bundleState );
		}

		$this->displayStateInfoMessage( $title, $bundleState );
		$out->setPageTitle( $this->msg( 'tpt-translation-settings-page-title' )->text() );
		$out->addWikiMsg( 'tpt-list-pages-in-translations' );
	}

	private function addPageForm(
		Title $target,
		string $formClass,
		string $action,
		?int $revision
	): void {
		$formParams = [
			'method' => 'post',
			'action' => $this->getPageTitle()->getLocalURL(),
			'class' => $formClass
		];

		$this->getOutput()->addHTML(
			Xml::openElement( 'form', $formParams ) .
			Html::hidden( 'do', $action ) .
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
			( $revision ? Html::hidden( 'revision', $revision ) : '' ) .
			Html::hidden( 'target', $target->getPrefixedText() ) .
			Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() )
		);
	}

	private function displayStateInfoMessage( Title $title, TranslatableBundleState $bundleState ): void {
		$stateId = $bundleState->getStateId();
		if ( $stateId === TranslatableBundleState::UNSTABLE ) {
			$infoMessage = $this->msg( 'tpt-translation-settings-unstable-notice' );
		} elseif ( $stateId === TranslatableBundleState::PROPOSE ) {
			$userHasPageTranslationRight = $this->getUser()->isAllowed( 'pagetranslation' );
			if ( $userHasPageTranslationRight ) {
				$infoMessage = $this->msg( 'tpt-translation-settings-proposed-pagetranslation-notice' )->params(
					'https://www.mediawiki.org/wiki/Special:MyLanguage/' .
					'Help:Extension:Translate/Page_translation_administration',
					$title->getFullURL( 'action=edit' ),
					SpecialPage::getTitleFor( 'PagePreparation' )
						->getFullURL( [ 'page' => $title->getPrefixedText() ] )
				);
			} else {
				$infoMessage = $this->msg( 'tpt-translation-settings-proposed-editor-notice' );
			}
		} else {
			$infoMessage = $this->msg( 'tpt-translation-settings-ignored-notice' );
		}

		$this->getOutput()->wrapWikiMsg( Html::noticeBox( '$1', '' ), $infoMessage );
	}

	private function getBlock( WebRequest $request, User $user, Title $title ): ?ErrorPageError {
		if ( $this->permissionManager->isBlockedFrom( $user, $title, !$request->wasPosted() ) ) {
			$block = $user->getBlock();
			if ( $block ) {
				return new UserBlockedError(
					$block,
					$user,
					$this->getLanguage(),
					$request->getIP()
				);
			}

			return new PermissionsError( 'pagetranslation', [ 'badaccess-group0' ] );
		}

		return null;
	}

	private function showTranslationStateRestricted(): void {
		$out = $this->getOutput();
		$out->wrapWikiMsg( Html::errorBox( "$1" ), 'tpt-translation-settings-restricted' );
		$out->addWikiMsg( 'tpt-list-pages-in-translations' );
	}
}
