/*******************************************************************************
 * Copyright (c) 2011, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.orion.internal.server.hosting;

import org.eclipse.orion.server.core.ProtocolConstants;

import org.eclipse.orion.internal.server.servlets.ServletResourceHandler;
import org.eclipse.orion.server.servlets.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.core.runtime.*;
import org.eclipse.orion.internal.server.servlets.*;
import org.eclipse.orion.server.core.OrionConfiguration;
import org.eclipse.orion.server.core.ServerStatus;
import org.eclipse.orion.server.core.metastore.UserInfo;
import org.eclipse.osgi.util.NLS;
import org.json.*;

/**
 * Processes an HTTP request for a site configuration resource.
 */
public class SiteConfigurationResourceHandler extends ServletResourceHandler<SiteInfo> {

	private final ServletResourceHandler<IStatus> statusHandler;

	public SiteConfigurationResourceHandler(ServletResourceHandler<IStatus> statusHandler) {
		this.statusHandler = statusHandler;
	}

	/**
	 * Creates a new site configuration with the given name, a generated id, and the rest of its 
	 * properties given by <code>object</code>. 
	 * @param user User creating the SiteConfiguration
	 * @param name Name for the SiteConfiguration
	 * @param workspace Workspace the SiteConfiguration is associated to
	 * @param object Object from which other properties will be drawn
	 * @return The created SiteConfiguration.
	 */
	public static SiteInfo createFromJSON(UserInfo user, String name, String workspace, JSONObject object) throws CoreException {
		if (name == null || name.length() == 0)
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Name is missing", null));
		else if (workspace == null || name.length() == 0)
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Workspace is missing", null));

		SiteInfo site = SiteInfo.newSiteConfiguration(user, name, workspace);
		copyProperties(object, site, false);
		site.save(user);
		return site;
	}

	/**
	 * Copies properties from a JSONObject representation of a site configuration to a SiteConfiguration
	 * instance.
	 * @param source JSON object to copy from.
	 * @param target Site configuration instance to copy to.
	 * @param copyName If <code>true</code>, the name property from <code>source</code> will overwrite
	 * <code>target</code>'s name.
	 * @throws CoreException if, after copying, <code>target</code> is missing a required property.
	 */
	private static void copyProperties(JSONObject source, SiteInfo target, boolean copyName) throws CoreException {
		if (copyName) {
			String name = source.optString(ProtocolConstants.KEY_NAME, null);
			if (name != null)
				target.setName(name);
		}

		String hostHint = source.optString(SiteConfigurationConstants.KEY_HOST_HINT, null);
		if (hostHint != null)
			target.setHostHint(hostHint);

		String workspace = source.optString(SiteConfigurationConstants.KEY_WORKSPACE, null);
		if (workspace != null)
			target.setWorkspace(workspace);

		JSONArray mappings = source.optJSONArray(SiteConfigurationConstants.KEY_MAPPINGS);
		if (mappings != null)
			target.setMappings(mappings);

		// Sanity check
		if (target.getName() == null || target.getName().length() == 0)
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Name was not specified", null));
		if (target.getWorkspace() == null || target.getWorkspace().length() == 0)
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Workspace was not specified", null));
	}

	/**
	 * @param baseLocation The URI of the SiteConfigurationServlet.
	 * @return Representation of <code>site</code> as a JSONObject.
	 */
	public static JSONObject toJSON(SiteInfo site, URI baseLocation) {
		JSONObject result = site.toJSON();
		try {
			result.put(ProtocolConstants.KEY_LOCATION, URIUtil.append(baseLocation, site.getId()));
			// Note: The SiteConfigurationConstants.KEY_HOSTING_STATUS field will be contributed to the result
			// by the site-hosting bundle (if present) via an IWebResourceDecorator
		} catch (JSONException e) {
			// Can't happen
		}
		return result;
	}

	private boolean handleGet(HttpServletRequest req, HttpServletResponse resp, SiteInfo site) throws IOException {
		// Strip off the SiteConfig id from the request URI
		URI location = getURI(req);
		URI baseLocation = location.resolve(""); //$NON-NLS-1$
		JSONObject result = toJSON(site, baseLocation);
		OrionServlet.writeJSONResponse(req, resp, result, JsonURIUnqualificationStrategy.LOCATION_ONLY);
		return true;
	}

	/**
	 * Creates a new site configuration, and possibly starts it.
	 * @param site <code>null</code>
	 */
	private boolean handlePost(HttpServletRequest req, HttpServletResponse resp, SiteInfo site) throws CoreException, IOException, JSONException {
		if (site != null)
			throw new IllegalArgumentException("Can't POST to an existing site");
		UserInfo user = OrionConfiguration.getMetaStore().readUser(getUserName(req));
		JSONObject requestJson = getRequestJson(req);
		try {
			site = doCreateSiteConfiguration(req, requestJson, user);
			changeHostingStatus(req, resp, requestJson, user, site);
		} catch (CoreException e) {
			// If starting it failed, try to clean up
			if (site != null) {
				// Remove site config
				site.delete(user);
			}
			throw e;
		}

		URI baseLocation = getURI(req);
		JSONObject result = toJSON(site, baseLocation);
		OrionServlet.writeJSONResponse(req, resp, result, JsonURIUnqualificationStrategy.LOCATION_ONLY);

		resp.setStatus(HttpServletResponse.SC_CREATED);
		resp.addHeader(ProtocolConstants.HEADER_LOCATION, result.getString(ProtocolConstants.KEY_LOCATION));
		return true;
	}

	private boolean handlePut(HttpServletRequest req, HttpServletResponse resp, SiteInfo site) throws IOException, CoreException, JSONException {
		UserInfo user = OrionConfiguration.getMetaStore().readUser(getUserName(req));
		JSONObject requestJson = OrionServlet.readJSONRequest(req);
		copyProperties(requestJson, site, true);

		// Start/stop the site if necessary
		changeHostingStatus(req, resp, requestJson, user, site);

		// Everything succeeded, save the changed site
		site.save(user);

		// Strip off the SiteConfig id from the request URI
		URI location = getURI(req);
		URI baseLocation = location.resolve(""); //$NON-NLS-1$

		JSONObject result = toJSON(site, baseLocation);
		OrionServlet.writeJSONResponse(req, resp, result, JsonURIUnqualificationStrategy.LOCATION_ONLY);
		return true;
	}

	private boolean handleDelete(HttpServletRequest req, HttpServletResponse resp, SiteInfo site) throws CoreException {
		UserInfo user = OrionConfiguration.getMetaStore().readUser(getUserName(req));
		ISiteHostingService hostingService = getHostingService();
		IHostedSite runningSite = hostingService.get(site, user);
		if (runningSite != null) {
			String msg = NLS.bind("Site configuration is running at {0}. Must be stopped before it can be deleted", runningSite.getHost());
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_CONFLICT, msg, null));
		}
		site.delete(user);
		return true;
	}

	@Override
	public boolean handleRequest(HttpServletRequest req, HttpServletResponse resp, SiteInfo site) throws ServletException {
		if (site == null && getMethod(req) != Method.POST) {
			return statusHandler.handleRequest(req, resp, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_NOT_FOUND, "Site configuration not found", null)); //$NON-NLS-1$
		}
		try {
			switch (getMethod(req)) {
				case GET :
					return handleGet(req, resp, site);
				case PUT :
					return handlePut(req, resp, site);
				case POST :
					return handlePost(req, resp, site);
				case DELETE :
					return handleDelete(req, resp, site);
				default :
					return false;
			}
		} catch (IOException e) {
			return statusHandler.handleRequest(req, resp, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage(), e));
		} catch (JSONException e) {
			return statusHandler.handleRequest(req, resp, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Syntax error in request", e));
		} catch (CoreException e) {
			return statusHandler.handleRequest(req, resp, e.getStatus());
		}
	}

	private static JSONObject getRequestJson(HttpServletRequest req) throws IOException, JSONException {
		//		WebUser user = WebUser.fromUserName(getUserName(req));
		return OrionServlet.readJSONRequest(req);
	}

	/**
	 * Creates a new site configuration from the request
	 */
	private SiteInfo doCreateSiteConfiguration(HttpServletRequest req, JSONObject requestJson, UserInfo user) throws CoreException {
		String name = computeName(req, requestJson);
		String workspace = requestJson.optString(SiteConfigurationConstants.KEY_WORKSPACE, null);
		SiteInfo site = createFromJSON(user, name, workspace, requestJson);
		return site;
	}

	/**
	 * Changes <code>site</code>'s hosting status to the desired status. The desired status 
	 * is given by the <code>HostingStatus.Status</code> field of the <code>requestJson</code>.
	 * @param site The site configuration to act on.
	 * @param requestJson The request body, which may or may not have a HostingStatus field.
	 * @throws CoreException If a bogus value was found for <code>HostingStatus.Status</code>,
	 * or if the hosting service threw an exception when trying to change the status.
	 */
	private void changeHostingStatus(HttpServletRequest req, HttpServletResponse resp, JSONObject requestJson, UserInfo user, SiteInfo site) throws CoreException {
		JSONObject hostingStatus = requestJson.optJSONObject(SiteConfigurationConstants.KEY_HOSTING_STATUS);
		if (hostingStatus == null)
			return;
		String status = hostingStatus.optString(SiteConfigurationConstants.KEY_HOSTING_STATUS_STATUS);
		try {
			if ("started".equalsIgnoreCase(status)) { //$NON-NLS-1$
				String editServer = req.getScheme() + "://" + req.getHeader("Host"); //$NON-NLS-1$ //$NON-NLS-2$
				getHostingService().start(site, user, editServer, new URI(req.getRequestURL().toString()));
			} else if ("stopped".equalsIgnoreCase(status)) { //$NON-NLS-1$
				getHostingService().stop(site, user);
			} else if (status == null) {
				// No Status; ignore it
			} else {
				// Status has a bogus value
				throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Status not understood: {0}", status), null));
			}
		} catch (SiteHostingException e) {
			// Give a JSON response object instead of stack trace
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage(), e));
		} catch (URISyntaxException e) {
			// Should not happen
			throw new CoreException(new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Error parsing request URL \"{0}\"", status), null));
		}
	}

	/**
	 * @throws CoreException If the site hosting service is not present.
	 */
	private static ISiteHostingService getHostingService() throws CoreException {
		ISiteHostingService service = HostingActivator.getDefault().getHostingService();
		if (service == null) {
			throw new CoreException(new Status(IStatus.ERROR, Activator.PI_SERVER_SERVLETS, "Site hosting service unavailable"));
		}
		return service;
	}

	/**
	 * Computes the name for the resource to be created by a POST operation.
	 * @return The name, or the empty string if no name was given.
	 */
	private static String computeName(HttpServletRequest req, JSONObject requestBody) {
		// Try Slug first
		String name = req.getHeader(ProtocolConstants.HEADER_SLUG);
		if (name == null || name.length() == 0) {
			name = requestBody.optString(ProtocolConstants.KEY_NAME);
		}
		return name;
	}

	/**
	 * Obtain and return the user name from the request headers.
	 */
	private static String getUserName(HttpServletRequest req) {
		return req.getRemoteUser();
	}
}