<?php

namespace MediaWiki\Extension\AbuseFilter\Pager;

use HtmlArmor;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Context\IContextSource;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
use MediaWiki\Html\Html;
use MediaWiki\Linker\Linker;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Pager\ReverseChronologicalPager;
use MediaWiki\Parser\Sanitizer;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentityValue;
use MediaWiki\WikiMap\WikiMap;
use stdClass;
use Wikimedia\Rdbms\IResultWrapper;

class AbuseLogPager extends ReverseChronologicalPager {
	/**
	 * @var array
	 */
	private $conds;

	/** @var string */
	private $basePageName;

	/**
	 * @var string[] Map of [ id => show|hide ], for entries that we're currently (un)hiding
	 */
	private $hideEntries;

	private LinkBatchFactory $linkBatchFactory;
	private PermissionManager $permissionManager;
	private AbuseFilterPermissionManager $afPermissionManager;
	private VariablesBlobStore $varBlobStore;

	public function __construct(
		IContextSource $context,
		LinkRenderer $linkRenderer,
		array $conds,
		LinkBatchFactory $linkBatchFactory,
		PermissionManager $permManager,
		AbuseFilterPermissionManager $afPermissionManager,
		VariablesBlobStore $varBlobStore,
		string $basePageName,
		array $hideEntries = []
	) {
		parent::__construct( $context, $linkRenderer );
		$this->conds = $conds;
		$this->linkBatchFactory = $linkBatchFactory;
		$this->permissionManager = $permManager;
		$this->afPermissionManager = $afPermissionManager;
		$this->varBlobStore = $varBlobStore;
		$this->basePageName = $basePageName;
		$this->hideEntries = $hideEntries;
	}

	/**
	 * @param stdClass $row
	 * @return string
	 */
	public function formatRow( $row ) {
		return $this->doFormatRow( $row );
	}

	/**
	 * @param stdClass $row
	 * @param bool $isListItem
	 * @return string
	 */
	public function doFormatRow( stdClass $row, bool $isListItem = true ): string {
		$performer = $this->getAuthority();
		$visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermissionManager );

		if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) {
			return '';
		}

		$linkRenderer = $this->getLinkRenderer();
		$diffLink = false;

		if ( !$row->afl_wiki ) {
			$title = Title::makeTitle( $row->afl_namespace, $row->afl_title );

			$pageLink = $linkRenderer->makeLink(
				$title,
				null,
				[],
				[ 'redirect' => 'no' ]
			);
			if ( $row->rev_id ) {
				$diffLink = $linkRenderer->makeKnownLink(
					$title,
					new HtmlArmor( $this->msg( 'abusefilter-log-diff' )->parse() ),
					[],
					[ 'diff' => 'prev', 'oldid' => $row->rev_id ]
				);
			} elseif (
				isset( $row->ar_timestamp ) && $row->ar_timestamp
				&& $this->canSeeUndeleteDiffForPage( $title )
			) {
				$diffLink = $linkRenderer->makeKnownLink(
					SpecialPage::getTitleFor( 'Undelete' ),
					new HtmlArmor( $this->msg( 'abusefilter-log-diff' )->parse() ),
					[],
					[
						'diff' => 'prev',
						'target' => $title->getPrefixedText(),
						'timestamp' => $row->ar_timestamp,
					]
				);
			}
		} else {
			$pageLink = WikiMap::makeForeignLink( $row->afl_wiki, $row->afl_title );

			if ( $row->afl_rev_id ) {
				$diffUrl = WikiMap::getForeignURL( $row->afl_wiki, $row->afl_title );
				$diffUrl = wfAppendQuery( $diffUrl,
					[ 'diff' => 'prev', 'oldid' => $row->afl_rev_id ] );

				$diffLink = Linker::makeExternalLink( $diffUrl,
					$this->msg( 'abusefilter-log-diff' )->text() );
			}
		}

		if ( !$row->afl_wiki ) {
			// Local user
			$userLink = SpecialAbuseLog::getUserLinks( $row->afl_user, $row->afl_user_text );
		} else {
			$userLink = WikiMap::foreignUserLink( $row->afl_wiki, $row->afl_user_text ) . ' ' .
				$this->msg( 'parentheses' )->params( WikiMap::getWikiName( $row->afl_wiki ) )->escaped();
		}

		$lang = $this->getLanguage();
		$timestamp = htmlspecialchars( $lang->userTimeAndDate( $row->afl_timestamp, $this->getUser() ) );

		$actions_takenRaw = $row->afl_actions;
		if ( !strlen( trim( $actions_takenRaw ) ) ) {
			$actions_taken = $this->msg( 'abusefilter-log-noactions' )->escaped();
		} else {
			$actions = explode( ',', $actions_takenRaw );
			$displayActions = [];

			$specsFormatter = AbuseFilterServices::getSpecsFormatter();
			$specsFormatter->setMessageLocalizer( $this->getContext() );
			foreach ( $actions as $action ) {
				$displayActions[] = $specsFormatter->getActionDisplay( $action );
			}
			$actions_taken = $lang->commaList( $displayActions );
		}

		$filterID = $row->afl_filter_id;
		$global = $row->afl_global;

		// Pull global filter description
		$lookup = AbuseFilterServices::getFilterLookup();
		try {
			$filterObj = $lookup->getFilter( $filterID, $global );
			$escaped_comments = Sanitizer::escapeHtmlAllowEntities( $filterObj->getName() );
		} catch ( CentralDBNotAvailableException $_ ) {
			$escaped_comments = $this->msg( 'abusefilter-log-description-not-available' )->escaped();
			// either hide all filters, including not hidden/protected, or show all, including hidden/protected
			// we choose the former
			$filterObj = MutableFilter::newDefault();
			$filterObj->setProtected( true );
			$filterObj->setHidden( true );
		}

		// Determine if the user has access to the associated filter and also the details of the current log
		// These are used to determine what links to display for the log line.
		$canSeeLogDetails = $this->afPermissionManager->canSeeLogDetailsForFilter( $performer, $filterObj );
		$canSeeFilter = $canSeeLogDetails;
		if ( $filterObj->isProtected() ) {
			$canSeeFilter = $canSeeFilter &&
				$this->afPermissionManager->canViewProtectedVariablesInFilter( $performer, $filterObj )->isGood();

			if ( $canSeeLogDetails ) {
				$vars = $this->varBlobStore->loadVarDump( $row );
				$canSeeLogDetails = $this->afPermissionManager->canViewProtectedVariables(
					$performer, array_keys( $vars->getVars() )
				)->isGood();
			}
		}

		if ( $canSeeFilter ) {
			$actionLinks = [];

			if ( $canSeeLogDetails ) {
				if ( $isListItem ) {
					$detailsLink = $linkRenderer->makeKnownLink(
						SpecialPage::getTitleFor( $this->basePageName, $row->afl_id ),
						$this->msg( 'abusefilter-log-detailslink' )->text()
					);
					$actionLinks[] = $detailsLink;
				}

				$examineTitle = SpecialPage::getTitleFor( 'AbuseFilter', 'examine/log/' . $row->afl_id );
				$examineLink = $linkRenderer->makeKnownLink(
					$examineTitle,
					new HtmlArmor( $this->msg( 'abusefilter-changeslist-examine' )->parse() )
				);
				$actionLinks[] = $examineLink;
			}

			if ( $diffLink ) {
				$actionLinks[] = $diffLink;
			}

			if ( !$isListItem && $canSeeLogDetails && $this->afPermissionManager->canHideAbuseLog( $performer ) ) {
				// Link for hiding a single entry from the details view
				$hideLink = $linkRenderer->makeKnownLink(
					SpecialPage::getTitleFor( $this->basePageName, 'hide' ),
					$this->msg( 'abusefilter-log-hidelink' )->text(),
					[],
					[ "hideids[$row->afl_id]" => 1 ]
				);

				$actionLinks[] = $hideLink;
			}

			if ( $global ) {
				$centralDb = $this->getConfig()->get( 'AbuseFilterCentralDB' );
				$linkMsg = $this->msg( 'abusefilter-log-detailedentry-global' )
					->numParams( $filterID );
				if ( $centralDb !== null ) {
					$globalURL = WikiMap::getForeignURL(
						$centralDb,
						'Special:AbuseFilter/' . $filterID
					);
					$filterLink = Linker::makeExternalLink( $globalURL, $linkMsg->text() );
				} else {
					$filterLink = $linkMsg->escaped();
				}
			} else {
				$title = SpecialPage::getTitleFor( 'AbuseFilter', (string)$filterID );
				$linkText = $this->msg( 'abusefilter-log-detailedentry-local' )
					->numParams( $filterID )->text();
				$filterLink = $linkRenderer->makeKnownLink( $title, $linkText );
			}

			if ( count( $actionLinks ) ) {
				$msg = 'abusefilter-log-detailedentry-meta';
			} else {
				$msg = 'abusefilter-log-detailedentry-meta-without-action-links';
			}
			$description = $this->msg( $msg )->rawParams(
				$timestamp,
				$userLink,
				$filterLink,
				htmlspecialchars( $row->afl_action ),
				$pageLink,
				$actions_taken,
				$escaped_comments,
				// Passing $8 to 'abusefilter-log-detailedentry-meta-without-action-links' will do nothing,
				// as it's not used.
				$lang->pipeList( $actionLinks )
			)->params( $row->afl_user_text )->parse();
		} else {
			if ( $diffLink ) {
				$msg = 'abusefilter-log-entry-withdiff';
			} else {
				$msg = 'abusefilter-log-entry';
			}
			$description = $this->msg( $msg )->rawParams(
				$timestamp,
				$userLink,
				htmlspecialchars( $row->afl_action ),
				$pageLink,
				$actions_taken,
				$escaped_comments,
				// Passing $7 to 'abusefilter-log-entry' will do nothing, as it's not used.
				$diffLink ?: ''
			)->params( $row->afl_user_text )->parse();
		}

		$attribs = [];
		if (
			$this->isHidingEntry( $row ) === true ||
			// If isHidingEntry is false, we've just unhidden the row
			( $this->isHidingEntry( $row ) === null && $row->afl_deleted )
		) {
			$attribs['class'] = 'mw-abusefilter-log-hidden-entry';
		} else {
			$attribs['data-afl-log-id'] = $row->afl_id;
		}
		if ( self::entryHasAssociatedDeletedRev( $row ) ) {
			$description .= ' ' .
				$this->msg( 'abusefilter-log-hidden-implicit' )->parse();
		}

		if ( $isListItem && !$this->hideEntries && $this->afPermissionManager->canHideAbuseLog( $performer ) ) {
			// Checkbox for hiding multiple entries, single entries are handled above
			$description = Html::check( 'hideids[' . $row->afl_id . ']' ) . ' ' . $description;
		}

		if ( $isListItem ) {
			return Html::rawElement( 'li', $attribs, $description );
		} else {
			return Html::rawElement( 'span', $attribs, $description );
		}
	}

	/**
	 * Can this user see diffs generated by Special:Undelete for the page?
	 * @see MediaWiki\Specials\SpecialUndelete
	 * @param LinkTarget $page
	 *
	 * @return bool
	 */
	private function canSeeUndeleteDiffForPage( LinkTarget $page ): bool {
		if ( !$this->canSeeUndeleteDiffs() ) {
			return false;
		}

		foreach ( [ 'deletedtext', 'undelete' ] as $action ) {
			if ( $this->permissionManager->userCan(
				$action, $this->getUser(), $page, PermissionManager::RIGOR_QUICK
			) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Can this user see diffs generated by Special:Undelete?
	 * @see MediaWiki\Specials\SpecialUndelete
	 *
	 * @return bool
	 */
	private function canSeeUndeleteDiffs(): bool {
		if ( !$this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
			return false;
		}

		return $this->permissionManager->userHasAnyRight(
			$this->getUser(), 'deletedtext', 'undelete' );
	}

	/**
	 * @return array
	 */
	public function getQueryInfo() {
		$info = [
			'tables' => [ 'abuse_filter_log', 'revision' ],
			'fields' => [
				'afl_id',
				'afl_global',
				'afl_filter_id',
				'afl_user',
				'afl_ip_hex',
				'afl_user_text',
				'afl_action',
				'afl_actions',
				'afl_var_dump',
				'afl_timestamp',
				'afl_namespace',
				'afl_title',
				'afl_wiki',
				'afl_deleted',
				'afl_rev_id',
				'rev_id',
			],
			'conds' => $this->conds,
			'join_conds' => [
				'revision' => [
					'LEFT JOIN',
					[
						'afl_wiki' => null,
						$this->mDb->expr( 'afl_rev_id', '!=', null ),
						'rev_id=afl_rev_id',
					]
				],
			],
		];

		if ( $this->canSeeUndeleteDiffs() ) {
			$info['tables'][] = 'archive';
			$info['fields'][] = 'ar_timestamp';
			$info['join_conds']['archive'] = [
				'LEFT JOIN',
				[
					'afl_wiki' => null,
					$this->mDb->expr( 'afl_rev_id', '!=', null ),
					'rev_id' => null,
					'ar_rev_id=afl_rev_id',
				]
			];
		}

		if ( !$this->afPermissionManager->canSeeHiddenLogEntries( $this->getAuthority() ) ) {
			$info['conds']['afl_deleted'] = 0;
		}

		return $info;
	}

	/**
	 * @param IResultWrapper $result
	 */
	protected function preprocessResults( $result ) {
		if ( $this->getNumRows() === 0 ) {
			return;
		}

		$lb = $this->linkBatchFactory->newLinkBatch();
		$lb->setCaller( __METHOD__ );
		foreach ( $result as $row ) {
			// Only for local wiki results
			if ( !$row->afl_wiki ) {
				$lb->add( $row->afl_namespace, $row->afl_title );
				$lb->addUser( new UserIdentityValue( $row->afl_user ?? 0, $row->afl_user_text ) );
			}
		}
		$lb->execute();
		$result->seek( 0 );
	}

	/**
	 * @param stdClass $row
	 * @return bool
	 * @todo This should be moved elsewhere
	 */
	private static function entryHasAssociatedDeletedRev( stdClass $row ): bool {
		if ( !$row->afl_rev_id ) {
			return false;
		}
		$revision = MediaWikiServices::getInstance()
			->getRevisionLookup()
			->getRevisionById( $row->afl_rev_id );
		return $revision && $revision->getVisibility() !== 0;
	}

	/**
	 * Check whether the entry passed in is being currently hidden/unhidden.
	 * This is used to format the entries list shown when updating visibility, and is necessary because
	 * when we decide whether to display the entry as hidden the DB hasn't been updated yet.
	 *
	 * @param stdClass $row
	 * @return bool|null True if just hidden, false if just unhidden, null if untouched
	 */
	private function isHidingEntry( stdClass $row ): ?bool {
		if ( isset( $this->hideEntries[ $row->afl_id ] ) ) {
			return $this->hideEntries[ $row->afl_id ] === 'hide';
		}
		return null;
	}

	/**
	 * @codeCoverageIgnore Merely declarative
	 * @inheritDoc
	 */
	public function getIndexField() {
		return 'afl_timestamp';
	}
}
