/* eslint-disable no-unused-vars */
/**
 * Represents a specific site
 *
 * @class GlobalWatchlistSiteBase
 * @abstract
 *
 * @param {GlobalWatchlistDebugger} globalWatchlistDebug Debugger instance to log to
 * @param {GlobalWatchlistLinker} linker Linker instance to use
 * @param {Object} config User configuration
 * @param {mw.ForeignApi} api Instance of mw.ForeignApi for this site
 * @param {GlobalWatchlistWatchlistUtils} watchlistUtils WatchlistUtils instance for this site
 * @param {string} urlFragment string for which site this represents
 */
function GlobalWatchlistSiteBase(
	globalWatchlistDebug,
	linker,
	config,
	api,
	watchlistUtils,
	urlFragment
) {
	// Logger to send debug info to
	this.debugLogger = globalWatchlistDebug;

	// User config and other settings, retrieved from getSettings
	this.config = config;

	// The api object to interact with
	this.apiObject = api;

	// Utility methods (GlobalWatchlistWatchlistUtils)
	this.watchlistUtils = watchlistUtils;

	// Site identifier in url format
	this.site = urlFragment;

	// Linker utility (GlobalWatchlistLinker)
	this.linker = linker;

	// Site identifier in format that can be used for element attributes
	this.siteID = encodeURIComponent( urlFragment.replace( /\./g, '_' ) );

	// Whether this site had any changes to show
	this.isEmpty = false;

	// Cached information about the tags of a site
	this.tags = {};

	// Whether there was an error when trying to use the API. To be able to use Promise.all,
	// API failures still resolve the Promise rather than rejecting it. If Promise.allSettled
	// becomes available for use, this should no longer be needed
	this.apiError = false;

	// Instance of GlobalWatchlistWikibaseHandler, only used for wikibase
	// Don't create it if it will never be needed
	if ( this.site === config.wikibaseSite ) {
		var GlobalWatchlistWikibaseHandler = require( './WikibaseHandler.js' );
		this.wikibaseHandler = new GlobalWatchlistWikibaseHandler(
			globalWatchlistDebug,
			api,
			config.lang
		);
	}
}

/**
 * Shortcut for sending information to the debug logger
 *
 * @param {string} msg Message for debug entry
 * @param {string} [extraInfo] Extra information for debug entry
 */
GlobalWatchlistSiteBase.prototype.debug = function ( msg, extraInfo ) {
	this.debugLogger.info( this.site + ':' + msg, extraInfo );
};

/**
 * Shortcut for sending errors to the debug logger
 *
 * @param {string} msg Message for error entry
 * @param {Object} data Extra information for error entry
 */
GlobalWatchlistSiteBase.prototype.error = function ( msg, data ) {
	this.debugLogger.error( this.site + ':' + msg, data );
};

/**
 * API handler for debugging and avoiding actual important actions when testing client-side
 *
 * @param {string} func Function name
 * @param {Object} content Content to send to the api
 * @param {string} name Name, for logging purposes
 * @return {Promise} Result of the api call
 */
GlobalWatchlistSiteBase.prototype.api = function ( func, content, name ) {
	var that = this;

	return new Promise( function ( resolve, reject ) {
		that.debug( 'API.' + name + ' (called), with func & content:', [ func, content ] );
		that.apiObject[ func ]( content ).then( function ( response ) {
			that.debug(
				'API.' + name + ' (result); func, content, & response',
				[ func, content, response ]
			);
			resolve( response );
		} ).catch( function ( code, data ) {
			that.error( 'API.' + name + ' ' + code, data );
			that.apiError = true;

			var $userNotification = $( '<div>' )
				.append(
					mw.msg( 'globalwatchlist-api-error', that.site ),
					that.apiObject.getErrorMessage( data )
				);

			mw.notify(
				$userNotification,
				{
					type: 'error',
					autoHide: false
				}
			);

			// See above on apiError for why this resolves instead of rejecting
			// since we don't know what exactly the caller was expected, just
			// resolve "error" and leave the handling for the caller
			resolve( 'ERROR' );
		} );
	} );
};

/**
 * Get the changes on a user's watchlist
 *
 * This method calls itself recursively until there are no remaining changes to retrieve,
 * using the `continue` functionality.
 *
 * @param {number} iteration iteration count
 * @param {string} continueFrom value of wlcontinue in the previous call
 * @return {Promise} Promise of api result
 */
GlobalWatchlistSiteBase.prototype.actuallyGetWatchlist = function ( iteration, continueFrom ) {
	var that = this;

	return new Promise( function ( resolve ) {
		var query = {
			action: 'query',
			formatversion: 2,
			list: 'watchlist',
			wllimit: 'max',
			wlprop: that.config.watchlistQueryProps,
			wlshow: that.config.watchlistQueryShow,
			wltype: that.config.watchlistQueryTypes
		};
		if ( iteration > 1 ) {
			query.wlcontinue = continueFrom;
		}
		if ( !that.config.fastMode ) {
			query.wlallrev = true;
		}

		that.api( 'get', query, 'actuallyGetWatchlist #' + iteration ).then( function ( response ) {
			if ( response === 'ERROR' ) {
				resolve( [] );
				return;
			}
			var wlraw = response.query.watchlist;
			if ( response.continue && response.continue.wlcontinue ) {
				that.actuallyGetWatchlist(
					iteration + 1,
					response.continue.wlcontinue
				).then( function ( innerResponse ) {
					// If there was an error in the recursive call, this just
					// adds an empty array. getWatchlist checks this.apiError
					// before assuming that an empty response means nothing to show
					resolve( wlraw.concat( innerResponse ) );
				} );
			} else {
				resolve( wlraw );
			}
		} );
	} );
};

/**
 * Update the strikethrough and text for entries being watched/unwatched
 *
 * Calls the API to actually unwatch/rewatch a page
 *
 * Calls `processUpdateWatched` to update the display (either add or remove the strikethrough,
 *   and update the text shown)
 *
 * If fast mode is not enabled, calls `getAssociatedTalkPage` to determine the talk/subject page
 *   associated with the one that was unwatched/rewatched, and then uses `processUpdateWatched`
 *   to update the display of any entries for the associated page
 *
 * @param {string} pageTitle Title of the page to watch or unwatch
 * @param {string} func Either 'watch' or 'unwatch'
 */
GlobalWatchlistSiteBase.prototype.changeWatched = function ( pageTitle, func ) {
	this.debug( 'changeWatched - Going to ' + func + ': ' + pageTitle );
	var that = this;
	this.api( func, pageTitle, 'updateWatched' );
	this.processUpdateWatched( pageTitle, func === 'unwatch' );
	if ( !this.config.fastMode ) {
		this.getAssociatedPageTitle( pageTitle ).then( function ( associatedTitle ) {
			that.processUpdateWatched( associatedTitle, func === 'unwatch' );
			// TODO re-add functionality for old checkChangesShown
		} );
	}
};

/**
 * Mark page as read
 *
 * Calls the API to reset notification timestamp for a page
 *
 * @param {string} pageTitle Title of the page to mark as read
 * @return {Promise} that resolves after the api call is made and after `afterMarkPageAsSeen`
 * is called, not necessarily after the api call is finished.
 */
GlobalWatchlistSiteBase.prototype.markPageAsSeen = function ( pageTitle ) {
	var that = this;

	return new Promise( function ( resolve ) {
		var setter = {
			action: 'setnotificationtimestamp',
			titles: pageTitle,
			timestamp: that.config.time.toISOString()
		};
		that.api( 'postWithEditToken', setter );

		that.afterMarkPageAsSeen( pageTitle );

		// Done within a promise so that display can ensure re-rendering occurs after
		// entries are updated
		resolve();
	} );
};

/**
 * Update display after marking a page as read
 *
 * Overriden in {@link GlobalWatchlistSiteDisplay}
 *
 * @param {string} pageTitle Page that was marked as read
 */
GlobalWatchlistSiteBase.prototype.afterMarkPageAsSeen = function ( pageTitle ) {
	// STUB
};

/**
 * Returns the talk/subject page associated with a given page, since entries for the associated page
 *   also need to have their text and strikethrough updated on unwatching/rewatching
 *
 * @param {string} pageTitle Title of the page for which to retrieve the associated page
 * @return {Promise} Promise of api result
 */
GlobalWatchlistSiteBase.prototype.getAssociatedPageTitle = function ( pageTitle ) {
	var that = this;
	return new Promise( function ( resolve ) {
		var query = {
			action: 'query',
			prop: 'info',
			titles: pageTitle,
			inprop: 'associatedpage',
			formatversion: 2
		};
		that.api( 'get', query, 'getAssociatedPageTitle' ).then( function ( response ) {
			resolve( response.query.pages[ 0 ].associatedpage );
		} );
	} );
};

/**
 * Get the tags for a wiki, loading them if not already available (in fast mode we don't retrieve
 * tags information for the watchlist, so this returns an empty object)
 *
 * Once this is called once, the tag info is stored in this.tags and future calls with return early
 *
 * @return {Promise} Resolves with the tags that where retrieved, or an empty object if we are
 *   in fast mode
 */
GlobalWatchlistSiteBase.prototype.getTagList = function () {
	var that = this;
	return new Promise( function ( resolve ) {
		if ( that.config.fastMode || Object.keys( that.tags ).length > 0 ) {
			// Either we are in fast mode, and we should return an empty object, which
			// is the default value of that.tags, or we already fetched the tags info
			// and its already available in that.tags
			resolve( that.tags );
		} else {
			var query = {
				action: 'query',
				list: 'tags',
				tglimit: 'max',
				tgprop: 'displayname'
			};
			that.api( 'get', query, 'getTags' ).then( function ( response ) {
				var asObject = {};
				response.query.tags.forEach( function ( tag ) {
					asObject[ tag.name ] = ( tag.displayname || false ) ?
						that.linker.fixLocalLinks( tag.displayname ) :
						tag.name;
				} );
				that.debug( 'getTagList', asObject );
				// Save for future calls (eg on refresh)
				that.tags = asObject;
				resolve( asObject );
			} );
		}
	} );
};

/**
 * Get the rendered changes for a user's watchlist
 *
 * @param {Object} latestConfig config, can change
 * @return {Promise} Promise that the watchlist was retrieved
 */
GlobalWatchlistSiteBase.prototype.getWatchlist = function ( latestConfig ) {
	this.config = latestConfig;
	var that = this;
	return new Promise( function ( resolve ) {
		that.actuallyGetWatchlist( 1, 0 ).then( function ( wlraw ) {
			if ( !( wlraw && wlraw[ 0 ] ) ) {
				if ( that.apiError ) {
					that.debug( 'getWatchlist - error' );

					// Include in the normal display section
					that.isEmpty = false;

					that.renderApiFailure();
				} else {
					that.debug( 'getWatchlist - empty' );
					that.isEmpty = true;
				}

				resolve();
				return;
			}
			// In case it was previously set to true
			that.isEmpty = false;

			that.debug( 'getWatchlist wlraw', wlraw );

			that.getTagList().then( function ( tagsInfo ) {
				var prelimSummary = that.watchlistUtils.rawToSummary(
					wlraw,
					that.config.groupPage,
					tagsInfo
				);
				that.debug( 'getWatchlist prelimSummary', prelimSummary );

				that.makeWikidataList( prelimSummary ).then( function ( summary ) {
					that.debug( 'getWatchlist summary', summary );
					that.renderWatchlist( summary );
					resolve();
				} );
			} );
		} );
	} );
};

/**
 * Display the watchlist
 *
 * Overriden in {@link GlobalWatchlistSiteDisplay}
 *
 * @param {GlobalWatchlistEntryBase[]} summary What should be rendered
 */
GlobalWatchlistSiteBase.prototype.renderWatchlist = function ( summary ) {
	// STUB
};

/**
 * Fetch and process wikibase labels when the watchlist is for wikidata
 *
 * @param {GlobalWatchlistEntryBase[]} summary Original summary, with page titles (Q1, P2, L3, etc.)
 * @return {Promise} Updated summary, with labels
 */
GlobalWatchlistSiteBase.prototype.makeWikidataList = function ( summary ) {
	var that = this;
	return new Promise( function ( resolve ) {
		if ( that.site !== that.config.wikibaseSite || that.config.fastMode ) {
			resolve( summary );
		} else {
			that.wikibaseHandler.addWikibaseLabels( summary ).then( function ( updatedSummary ) {
				resolve( updatedSummary );
			} );
		}
	} );
};

/**
 * Mark a site as seen
 *
 * @return {Promise} that resolves after the api call is made and after `afterMarkAllAsSeen`
 *   is called, not necessarily after the api call is finished.
 */
GlobalWatchlistSiteBase.prototype.markAllAsSeen = function () {
	this.debug( 'markSiteAsSeen - marking' );
	var that = this;

	return new Promise( function ( resolve ) {
		var setter = {
			action: 'setnotificationtimestamp',
			entirewatchlist: true,
			timestamp: that.config.time.toISOString()
		};
		that.api( 'postWithEditToken', setter, 'actuallyMarkSiteAsSeen' );

		that.afterMarkAllAsSeen();

		// Done within a promise so that display can ensure re-rendering occurs after
		// entries are updated
		resolve();
	} );
};

/**
 * Update display after making a site as seen
 *
 * Overriden in {@link GlobalWatchlistSiteDisplay}
 */
GlobalWatchlistSiteBase.prototype.afterMarkAllAsSeen = function () {
	// STUB
};

/**
 * Update entry click handlers, text, and strikethrough for a specific title
 *
 * Overriden in {@link GlobalWatchlistSiteDisplay}
 *
 * @param {string} pageTitle Title of the page that was unwatched/rewatched.
 * @param {boolean} unwatched Whether the page was unwatched
 */
GlobalWatchlistSiteBase.prototype.processUpdateWatched = function ( pageTitle, unwatched ) {
	// STUB
};

/**
 * Used by {@link GlobalWatchlistSiteDisplay} to still include an output for api failures
 */
GlobalWatchlistSiteBase.prototype.renderApiFailure = function () {
	// STUB
};

module.exports = GlobalWatchlistSiteBase;