<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\MessageBundleTranslation;

use Job;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscription;
use MediaWiki\Extension\Translate\MessageLoading\RebuildMessageIndexJob;
use MediaWiki\Extension\Translate\Services;
use MediaWiki\Extension\Translate\Statistics\MessageGroupStats;
use MediaWiki\Extension\Translate\Synchronization\UpdateMessageJob;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;

/**
 * @author Niklas Laxström
 * @license GPL-2.0-or-later
 * @since 2021.12
 */
class UpdateMessageBundleJob extends Job {
	/** @inheritDoc */
	public function __construct( Title $title, $params = [] ) {
		parent::__construct( 'UpdateMessageBundle', $title, $params );
	}

	public static function newJob( Title $bundlePageTitle, int $revisionId, ?int $previousRevisionId ): self {
		return new self(
			$bundlePageTitle,
			[
				'revisionId' => $revisionId,
				'previousRevisionId' => $previousRevisionId,
			]
		);
	}

	/** @inheritDoc */
	public function run(): bool {
		$mwInstance = MediaWikiServices::getInstance();
		$lb = $mwInstance->getDBLoadBalancerFactory();
		$jobQueue = $mwInstance->getJobQueueGroup();
		$logger = LoggerFactory::getInstance( 'Translate.MessageBundle' );
		$messageIndex = Services::getInstance()->getMessageIndex();
		$messageGroupSubscription = Services::getInstance()->getMessageGroupSubscription();

		$logger->info( 'UpdateMessageBundleJob: Starting job for: ' . $this->getTitle()->getPrefixedText() );

		// Not sure if this is necessary, but it should ensure that this job, which was created
		// when a revision was saved, can read that revision from the replica. In addition, this
		// may potentially do a bunch of more writes that could cause more replication lag.
		if ( !$lb->waitForReplication() ) {
			$logger->warning( 'UpdateMessageBundleJob: Continuing despite replication lag' );
		}

		// Setup
		$bundlePageTitle = $this->getTitle();
		$name = $bundlePageTitle->getPrefixedText();
		$pageId = $bundlePageTitle->getId();
		$groupId = MessageBundleMessageGroup::getGroupId( $name );
		$params = $this->getParams();
		// We don't care about the group description or label, so no need to pass it through
		$group = new MessageBundleMessageGroup(
			$groupId, $name, $pageId, $params['revisionId'], null, null
		);
		$messages = $group->getDefinitions();
		$previousMessages = [];
		if ( $params['previousRevisionId'] ) {
			$groupPreviousVersion = new MessageBundleMessageGroup(
				$groupId, $name, $pageId, $params['previousRevisionId'], null, null
			);
			$previousMessages = $groupPreviousVersion->getDefinitions();
		}

		// Fill in the front-cache. Ideally this should be done right away, but hopefully
		// this is okay since we only trigger message group cache rebuild later in this job.
		// It's possible that some other change triggers it earlier and makes the new group
		// available before this step is complete.
		$newKeys = array_diff( array_keys( $messages ), array_keys( $previousMessages ) );
		$messageIndex->storeInterim( $group, $newKeys );

		// Create jobs that will update the '/' source language pages. These pages should
		// exist so that the editor can show differences for changed messages. Also compare
		// against previous version (if any) to determine whether to mark translations as
		// outdated. There is no support for renames.
		$jobs = [];
		$namespace = $group->getNamespace();
		$code = $group->getSourceLanguage();
		foreach ( $messages as $key => $value ) {
			$title = Title::makeTitle( $namespace, "$key/$code" );
			$subscriptionState = $this->getMessageSubscriptionState( $previousMessages, $newKeys, $key, $value );
			$fuzzy = $subscriptionState === null;
			$jobs[] = UpdateMessageJob::newJob( $title, $value, $fuzzy );

			if ( $subscriptionState ) {
				$messageGroupSubscription->queueMessage(
					$title,
					$subscriptionState,
					[ $groupId ],
				);
			}
		}
		$jobQueue->push( $jobs );
		$logger->info(
			'UpdateMessageBundleJob: Added {number} UpdateMessageJobs to the job queue for: {title}',
			[
				'number' => count( $jobs ),
				'title' => $name
			]
		);

		// TODO: Ideally we would only invalidate message bundle message group cache
		MessageGroups::singleton()->recache();

		// Schedule message index update. Thanks to front caching, it is okay if this takes
		// a while (and on large wikis it does take a while!). Running it as a separate job
		// also allows de-duplication.
		$job = RebuildMessageIndexJob::newJob();
		$jobQueue->push( $job );
		$logger->info(
			'UpdateMessageBundleJob: {title}: Queue RebuildMessageIndexJob',
			[ 'title' => $name ]
		);

		// Refresh or fill translations statistics. If this a new group, this prevents
		// calculating the stats on the fly during read requests. If an existing group, this
		// makes sure that the statistics are up-to-date.
		MessageGroupStats::forGroup(
			$groupId,
			MessageGroupStats::FLAG_NO_CACHE | MessageGroupStats::FLAG_IMMEDIATE_WRITES
		);

		$messageGroupSubscription->queueNotificationJob();

		return true;
	}

	/**
	 * Return a message subscription state based on whether a message is new, updated
	 * or null if it hasn't been changed at all.
	 */
	private function getMessageSubscriptionState(
		?array $previousMessages,
		array $newKeys,
		string $key,
		string $value
	): ?string {
		if ( in_array( $key, $newKeys ) ) {
			return MessageGroupSubscription::STATE_ADDED;
		}

		$previousValue = $previousMessages[$key] ?? null;
		$isFuzzy = $previousMessages !== null && $previousValue !== $value;
		if ( $isFuzzy ) {
			return MessageGroupSubscription::STATE_UPDATED;
		}

		return null;
	}
}
