/**
 * Copyright (C) 2015 Orion Health (Orchestral Development Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package xbdd.webapp.resource.feature;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;

import xbdd.util.StatusHelper;
import xbdd.webapp.factory.MongoDBAccessor;
import xbdd.webapp.util.Coordinates;
import xbdd.webapp.util.Field;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;

@Path("/rest/feature")
public class Feature {

	private final MongoDBAccessor client;
	private static int MAX_ENVIRONMENTS_FOR_A_PRODUCT = 10;

	@Inject
	public Feature(final MongoDBAccessor client) {
		this.client = client;
	}

	@SuppressWarnings("unchecked")
	/**
	 * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id
	 *
	 * @param String featureId The featureId to get the history for
	 * @return DBObjet Returns the past feature status for the given featureId
	 */
	@GET
	@Path("/rollup/{product}/{major}.{minor}.{servicePack}/{featureId:.+}")
	public DBObject getFeatureRollup(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId) {
		final List<BasicDBObject> features = new ArrayList<BasicDBObject>();
		final DB db = this.client.getDB("bdd");
		final DBCollection collection = db.getCollection("features");
		final DBCollection summary = db.getCollection("summary");
		final BasicDBObject example = coordinates.getRollupQueryObject(featureId);
		final DBCursor cursor = collection.find(example,
				new BasicDBObject("id", 1).append("coordinates.build", 1).append("calculatedStatus", 1)
						.append("originalAutomatedStatus", 1).append("statusLastEditedBy", 1));
		try {
			while (cursor.hasNext()) {
				final DBObject doc = cursor.next();
				final BasicDBObject rollup = new BasicDBObject()
						.append("build", ((DBObject) doc.get("coordinates")).get("build"))
						.append("calculatedStatus", doc.get("calculatedStatus"))
						.append("originalAutomatedStatus", doc.get("originalAutomatedStatus"))
						.append("statusLastEditedBy", doc.get("statusLastEditedBy"));
				features.add(rollup);
			}
		} finally {
			cursor.close();
		}
		final BasicDBObject returns = new BasicDBObject()
				.append("coordinates", coordinates.getRollupCoordinates().append("featureId", featureId).append("version", coordinates.getVersionString()));
		
		final DBObject buildOrder = summary.findOne(coordinates.getQueryObject());
		final List<String> buildArray = (List<String>) buildOrder.get("builds");
		final List<BasicDBObject> orderedFeatures = new ArrayList<BasicDBObject>();
		
		for (String build : buildArray) {
			for (BasicDBObject feature : features) {
				if (feature.get("build").equals(build)) {
					orderedFeatures.add(feature);
					break;
				}
			}
		}
		
		returns.append("rollup", orderedFeatures);
		
		return returns;
	}

	/**
	 * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id
	 *
	 * @param String featureId The featureId to get the history for
	 * @return DBObjet Returns the the current features state and details (environments, tips, steps and scenarios)
	 */
	@GET
	@Path("/{product}/{major}.{minor}.{servicePack}/{build}/{featureId:.+}")
	public DBObject getFeature(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId) {
		final DB db = this.client.getDB("bdd");
		final DBCollection tips = db.getCollection("features");
		final BasicDBObject example = new BasicDBObject().append("id", featureId).append("coordinates", coordinates.getReportCoordinates());
		final DBObject feature = tips.findOne(example);
		if (feature != null) {
			Feature.embedTestingTips(feature, coordinates, db);
		}
		return feature;
	}

	@SuppressWarnings("unchecked")
	protected void updateTestingTips(final DB db, final Coordinates coordinates, final String featureId, final DBObject feature) {
		final DBCollection tips = db.getCollection("testingTips");
		final List<DBObject> elements = (List<DBObject>) feature.get("elements");
		for (final DBObject scenario : elements) {
			if (scenario.get("testing-tips") != null) {
				final String tipText = (String) scenario.get("testing-tips");
				final String scenarioId = (String) scenario.get("id");
				final BasicDBObject tipQuery = coordinates.getTestingTipsCoordinatesQueryObject(featureId, scenarioId);
				DBObject oldTip = null;
				// get the most recent tip that is LTE to the current coordinates. i.e. sort in reverse chronological order and take the
				// first item (if one exists).
				final DBCursor oldTipCursor = tips.find(tipQuery)
						.sort(new BasicDBObject("coordinates.major", -1).append("coordinates.minor", -1)
								.append("coordinates.servicePack", -1).append("coordinates.build", -1)).limit(1);
				try {
					if (oldTipCursor.hasNext()) {
						oldTip = oldTipCursor.next();
					}
				} finally {
					oldTipCursor.close();
				}
				if (oldTip != null) { // if there is an old tip...
					final String oldTipText = (String) oldTip.get("testing-tips"); // get it and...
					if (!tipText.equals(oldTipText)) {// compare it to the current tip to it, if they're not the same...
						final DBObject newTip = new BasicDBObject("testing-tips", tipText).append("coordinates",
								coordinates.getTestingTipsCoordinates(featureId, scenarioId))
								.append("_id", coordinates.getTestingTipsId(featureId, scenarioId));
						tips.save(newTip);// then save this as a new tip.
					}
				} else { // no prior tip exists, add this one.
					final DBObject newTip = new BasicDBObject("testing-tips", tipText).append("coordinates",
							coordinates.getTestingTipsCoordinates(featureId, scenarioId))
							.append("_id", coordinates.getTestingTipsId(featureId, scenarioId));
					tips.save(newTip);// then save this as a new tip.
				}
			}
			scenario.removeField("testing-tips");
		}
	}

	/**
	 * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id
	 *
	 * @param String featureId The featureId to make changes to
	 * @return DBObjet Returns the the features new state if changes were made and returns null if bad JSON was sent
	 */
	@PUT
	@Path("/{product}/{major}.{minor}.{servicePack}/{build}/{featureId:.+}")
	@Consumes("application/json")
	public DBObject putFeature(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId,
			@Context final HttpServletRequest req, final DBObject feature) {
		feature.put("calculatedStatus", StatusHelper.getFeatureStatus(feature));
		try {
			final DB db = this.client.getDB("bdd");
			final DBCollection collection = db.getCollection("features");
			final BasicDBObject example = coordinates.getReportCoordinatesQueryObject().append("id", featureId);
			final DBObject report = collection.findOne(example);

			// get the differences/new edits

			// Detect if the edits caused a change
			feature.put("statusLastEditedBy", req.getRemoteUser());
			feature.put("lastEditOn", new Date());
			final BasicDBList edits = updateEdits(feature, report);
			feature.put("edits", edits);

			updateTestingTips(db, coordinates, featureId, feature); // save testing tips / strip them out of the document.
			updateEnvironmentDetails(db, coordinates, feature);
			collection.save(feature);
			Feature.embedTestingTips(feature, coordinates, db); // rembed testing tips.
			return feature;// pull back feature - will re-include tips that were extracted prior to saving
		} catch (final Throwable th) {
			th.printStackTrace();
			return null;
		}
	}

	/**
	 * Goes through each environment detail on this feature and pushes each unique one to a per-product document in the 'environments'
	 * collection.
	 *
	 * @param db
	 * @param coordinates
	 * @param feature
	 */
	@SuppressWarnings("unchecked")
	public void updateEnvironmentDetails(final DB db, final Coordinates coordinates, final DBObject feature) {
		final DBCollection env = db.getCollection("environments");
		final List<DBObject> elements = (List<DBObject>) feature.get("elements");
		final BasicDBObject envQuery = coordinates.getQueryObject(Field.PRODUCT);
		// pull back the "product" document containing all the environments.
		DBObject productEnvironments = env.findOne(envQuery);
		// if one doesn't exist then create it.
		if (productEnvironments == null) {
			productEnvironments = new BasicDBObject();
			productEnvironments.put("coordinates", coordinates.getObject(Field.PRODUCT));
		}
		// pull back the list of environments
		List<Object> envs = (List<Object>) productEnvironments.get("environments");
		// if the list doesn't exist then create it.
		if (envs == null) {
			envs = new BasicDBList();
			productEnvironments.put("environments", envs);
		}
		final List<String> titleCache = new ArrayList<String>();
		// go through each scenario, pull out the environment details and add them to the back of the list.
		for (final DBObject scenario : elements) {
			String notes = (String) scenario.get("environment-notes");
			if (notes != null) {
				notes = notes.trim();
				if (notes.length() > 0) {
					if (!titleCache.contains(notes)) {
						titleCache.add(notes);
					}
				}
			}
		}
		// go through each unique environment detail, remove it if it is already in the list and append to the end.
		for (final String environmentDetail : titleCache) {
			envs.remove(environmentDetail);
			envs.add(environmentDetail);
		}
		// if the list gets too long, truncate it on a LRU basis.
		if (envs.size() > MAX_ENVIRONMENTS_FOR_A_PRODUCT) {
			envs = envs.subList(envs.size() - MAX_ENVIRONMENTS_FOR_A_PRODUCT, envs.size());
			productEnvironments.put("environments", envs);
		}
		// save the list back.
		env.save(productEnvironments);
	}

	@SuppressWarnings("unchecked")
	public static void embedTestingTips(final DBObject feature, final Coordinates coordinates, final DB db) {
		final DBCollection tips = db.getCollection("testingTips");
		final List<DBObject> elements = (List<DBObject>) feature.get("elements");
		for (final DBObject scenario : elements) {
			DBObject oldTip = null;
			final BasicDBObject tipQuery = coordinates.getTestingTipsCoordinatesQueryObject((String) feature.get("id"), (String) scenario.get("id"));
			// get the most recent tip that is LTE to the current coordinates. i.e. sort in reverse chronological order and take the first
			// item (if one exists).
			final DBCursor oldTipCursor = tips.find(tipQuery)
					.sort(new BasicDBObject("coordinates.major", -1).append("coordinates.minor", -1)
							.append("coordinates.servicePack", -1).append("coordinates.build", -1)).limit(1);
			try {
				if (oldTipCursor.hasNext()) {
					oldTip = oldTipCursor.next();
					scenario.put("testing-tips", oldTip.get("testing-tips"));
				}
			} finally {
				oldTipCursor.close();
			}
		}
	}

	private BasicDBList updateEdits(final DBObject feature, final DBObject previousVersion) {
		BasicDBList edits = (BasicDBList) feature.get("edits");
		if (edits == null) {
			edits = new BasicDBList();
		}
		final BasicDBList newEdits = new BasicDBList();
		final BasicDBObject edit = new BasicDBObject()
				.append("name", feature.get("statusLastEditedBy"))
				.append("date", feature.get("lastEditOn"))
				.append("prev", previousVersion.get("calculatedStatus"))
				.append("curr", feature.get("calculatedStatus"))
				.append("stepChanges",
						constructEditStepChanges(feature, previousVersion));
		newEdits.add(edit);
		newEdits.addAll(edits);
		return newEdits;
	}

	private BasicDBList constructEditStepChanges(final DBObject currentVersion, final DBObject previousVersion) {
		final BasicDBList stepChanges = new BasicDBList();
		final BasicDBList elements = (BasicDBList) currentVersion.get("elements");
		final BasicDBList prevElements = (BasicDBList) previousVersion.get("elements");
		if (elements != null) {
			for (int i = 0; i < elements.size(); i++) {
				final BasicDBList allSteps = new BasicDBList();
				final BasicDBList changes = new BasicDBList();
				final BasicDBObject element = (BasicDBObject) elements.get(i);
				final BasicDBObject prevElement = (BasicDBObject) prevElements.get(i);
				final String scenarioName = (String) element.get("name");
				boolean currManual = false;
				boolean prevManual = false;

				// get all scenario steps
				if ((BasicDBObject) element.get("background") != null) {
					for (int j = 0; j < ((BasicDBList) ((BasicDBObject) element.get("background")).get("steps")).size(); j++) {
						final BasicDBObject step = (BasicDBObject) ((BasicDBList) ((BasicDBObject) element.get("background")).get("steps"))
								.get(j);
						final BasicDBObject prevStep = (BasicDBObject) ((BasicDBList) ((BasicDBObject) prevElement.get("background"))
								.get("steps"))
								.get(j);
						final String id = (String) step.get("keyword") + (String) step.get("name");
						if (((BasicDBObject) step.get("result")).get("manualStatus") != null) {
							currManual = true;
						}
						if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) {
							prevManual = true;
						}
						final BasicDBObject compareStep = new BasicDBObject()
								.append("id", id)
								.append("curr", step)
								.append("prev", prevStep);
						allSteps.add(compareStep);
					}
				}

				if ((BasicDBList) element.get("steps") != null) {
					for (int j = 0; j < ((BasicDBList) element.get("steps")).size(); j++) {
						final BasicDBObject step = (BasicDBObject) ((BasicDBList) element.get("steps")).get(j);
						final BasicDBObject prevStep = (BasicDBObject) ((BasicDBList) prevElement.get("steps")).get(j);
						final String id = (String) step.get("keyword") + (String) step.get("name");
						if (((BasicDBObject) step.get("result")).get("manualStatus") != null) {
							currManual = true;
						}
						if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) {
							prevManual = true;
						}
						final BasicDBObject compareStep = new BasicDBObject()
								.append("id", id)
								.append("curr", step)
								.append("prev", prevStep);
						allSteps.add(compareStep);
					}
				}

				for (int j = 0; j < allSteps.size(); j++) {
					formatStep(changes, (BasicDBObject) allSteps.get(j), currManual, prevManual);
				}

				// only add if changes have been made
				if (changes.size() > 0) {
					final BasicDBObject singleScenario = new BasicDBObject()
							.append("scenario", scenarioName)
							.append("changes", changes);
					stepChanges.add(singleScenario);
				}
			}
		}
		return stepChanges;
	}

	private void formatStep(final BasicDBList changes, final BasicDBObject step,
			final boolean currManual, final boolean prevManual) {
		String currState, currCause, prevState, prevCause;

		final BasicDBObject currStep = ((BasicDBObject) step.get("curr"));
		if (((BasicDBObject) currStep.get("result")).get("manualStatus") != null) {
			currState = (String) ((BasicDBObject) currStep.get("result")).get("manualStatus");
			currCause = "manual";
		} else {
			currCause = "auto";
			if (currManual) {
				currState = "undefined";
			} else {
				currState = (String) ((BasicDBObject) currStep.get("result")).get("status");
			}
		}

		final BasicDBObject prevStep = ((BasicDBObject) step.get("prev"));
		if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) {
			prevState = (String) ((BasicDBObject) prevStep.get("result")).get("manualStatus");
			prevCause = "manual";
		} else {
			prevCause = "auto";
			if (prevManual) {
				prevState = "undefined";
			} else {
				prevState = (String) ((BasicDBObject) prevStep.get("result")).get("status");
			}
		}

		// only add if different
		if (!currState.equals(prevState) || !currCause.equals(prevCause)) {
			final BasicDBObject stateChange = new BasicDBObject()
					.append("id", step.get("id"))
					.append("curr", currState)
					.append("prev", prevState);
			changes.add(stateChange);
		}
	}
}