/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vitro.webapp.search.controller;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.RequestedAction;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexer;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStatus;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStatus.RebuildCounts;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStatus.State;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStatus.StatementCounts;
import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexerStatus.UriCounts;
import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingServiceSetup;

/**
 * Accepts requests to display the current status of the search index, or to
 * initiate a rebuild.
 * 
 * A DISPLAY or REBUILD request is handled like any other FreemarkerHttpServlet.
 * A STATUS is an AJAX request, we override doGet() so we can format the
 * template without enclosing it in a body template.
 * 
 * When initialized, this servlet adds a listener to the SearchIndexer, so it
 * can maintain a history of activity. This will provide the contents of the
 * display.
 */
@WebServlet(name = "IndexController", urlPatterns = {"/SearchIndex"} )
public class IndexController extends FreemarkerHttpServlet {
	private static final Log log = LogFactory.getLog(IndexController.class);

	/**
	 * <pre>
	 * This request might be:
	 * DISPLAY (default) -- Send the template that will contain the status display. 
	 * STATUS  -- Send the current status and history.
	 * REBUILD -- Initiate a rebuild. Then act like DISPLAY.
	 * </pre>
	 */
	private enum RequestType {
		DISPLAY, STATUS, REBUILD;

		/** What type of request is this? */
		static RequestType fromRequest(HttpServletRequest req) {
			if (hasParameter(req, "rebuild")) {
				return REBUILD;
			} else if (hasParameter(req, "status")) {
				return STATUS;
			} else {
				return DISPLAY;
			}
		}

		private static boolean hasParameter(HttpServletRequest req, String key) {
			String value = req.getParameter(key);
			return (value != null) && (!value.isEmpty());
		}
	}

	private static final String PAGE_URL = "/SearchIndex";
	private static final String PAGE_TEMPLATE_NAME = "searchIndex.ftl";
	private static final String STATUS_TEMPLATE_NAME = "searchIndexStatus.ftl";

	public static final RequestedAction REQUIRED_ACTIONS = SimplePermission.MANAGE_SEARCH_INDEX.ACTION;

	private SearchIndexer indexer;
	private static IndexHistory history;

	@Override
	public void init() throws ServletException {
		this.indexer = ApplicationUtils.instance().getSearchIndexer();
		super.init();
	}

	/**
	 * Called by SearchIndexerSetup to provide a history that dates from
	 * startup, not just from servlet load time.
	 */
	public static void setHistory(IndexHistory history) {
		IndexController.history = history;
	}

	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		switch (RequestType.fromRequest(req)) {
		case STATUS:
			showStatus(req, resp);
			break;
		default:
			super.doGet(req, resp);
			break;
		}
	}

	@Override
	protected String getTitle(String siteName, VitroRequest vreq) {
		return "Rebuild Search Index";
	}

	@Override
	protected AuthorizationRequest requiredActions(VitroRequest vreq) {
		return REQUIRED_ACTIONS;
	}

	@Override
	protected ResponseValues processRequest(VitroRequest vreq) {
		switch (RequestType.fromRequest(vreq)) {
		case REBUILD:
			requestRebuild();
			try {
				// Pause, giving a chance to start the task queue.
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// Don't care, so pass it along.
				Thread.currentThread().interrupt();
			}
			return new RedirectResponseValues(PAGE_URL);
		default:
			return showDisplay();
		}
	}

	private ResponseValues showDisplay() {
		HashMap<String, Object> body = new HashMap<>();
		body.put("statusUrl", UrlBuilder.getUrl(PAGE_URL, "status", "true"));
		body.put("rebuildUrl", UrlBuilder.getUrl(PAGE_URL, "rebuild", "true"));
		return new TemplateResponseValues(PAGE_TEMPLATE_NAME, body);
	}

	private void showStatus(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
		if (!PolicyHelper.isAuthorizedForActions(req, REQUIRED_ACTIONS)) {
			resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
			resp.getWriter().write(
					"You are not authorized to access this page.");
			return;
		}

		try {
			Map<String, Object> body = new HashMap<>();
			body.put("statusUrl", UrlBuilder.getUrl(PAGE_URL, "status", "true"));
			body.put("rebuildUrl",
					UrlBuilder.getUrl(PAGE_URL, "rebuild", "true"));
			body.put("status", buildStatusMap(indexer.getStatus()));
			if (history != null) {
				body.put("history", history.toMaps());
			}

			String rendered = FreemarkerProcessingServiceSetup.getService(
					getServletContext()).renderTemplate(STATUS_TEMPLATE_NAME,
					body, req);
			resp.getWriter().write(rendered);
		} catch (Exception e) {
			resp.setStatus(500);
			resp.getWriter().write(e.toString());
			log.error(e, e);
		}
	}

	private void requestRebuild() {
		indexer.rebuildIndex();
	}

	private Map<String, Object> buildStatusMap(SearchIndexerStatus status) {
		Map<String, Object> map = new HashMap<>();
		State state = status.getState();
		map.put("statusType", state);
		map.put("since", status.getSince());

		if (state == State.PROCESSING_URIS) {
			UriCounts counts = status.getCounts().asUriCounts();
			map.put("updated", counts.getUpdated());
			map.put("deleted", counts.getDeleted());
			map.put("remaining", counts.getRemaining());
			map.put("total", counts.getTotal());
			map.put("elapsed", breakDownElapsedTime(status.getSince()));
			map.put("expectedCompletion",
					figureExpectedCompletion(status.getSince(),
							counts.getTotal(),
							counts.getTotal() - counts.getRemaining()));
		} else if (state == State.PROCESSING_STMTS) {
			StatementCounts counts = status.getCounts().asStatementCounts();
			map.put("processed", counts.getProcessed());
			map.put("remaining", counts.getRemaining());
			map.put("total", counts.getTotal());
			map.put("elapsed", breakDownElapsedTime(status.getSince()));
			map.put("expectedCompletion",
					figureExpectedCompletion(status.getSince(),
							counts.getTotal(), counts.getProcessed()));
		} else if (state == State.REBUILDING) {
			RebuildCounts counts = status.getCounts().asRebuildCounts();
			map.put("documentsBefore", counts.getDocumentsBefore());
			map.put("documentsAfter", counts.getDocumentsAfter());
		} else {
			// nothing for IDLE or SHUTDOWN, except what's already there.
		}

		return map;
	}

	private Date figureExpectedCompletion(Date startTime, long totalToDo,
			long completedCount) {
		Date now = new Date();
		long elapsedMillis = now.getTime() - startTime.getTime();
		if (elapsedMillis <= 0) {
			return now;
		}
		if (completedCount <= 0) {
			return now;
		}
		if (totalToDo <= completedCount) {
			return now;
		}

		long millisPerRecord = elapsedMillis / completedCount;
		long expectedDuration = totalToDo * millisPerRecord;
		return new Date(expectedDuration + startTime.getTime());
	}

	private int[] breakDownElapsedTime(Date since) {
		long elapsedMillis = new Date().getTime() - since.getTime();
		long seconds = (elapsedMillis / 1000L) % 60L;
		long minutes = (elapsedMillis / 60000L) % 60L;
		long hours = elapsedMillis / 3600000L;
		return new int[] { (int) hours, (int) minutes, (int) seconds };
	}
}
