/*
 * Copyright 2011 gitblit.com.
 *
 * 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 com.gitblit.utils;

import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gitblit.Constants;
import com.gitblit.Constants.FederationProposalResult;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.google.gson.reflect.TypeToken;

/**
 * Utility methods for federation functions.
 *
 * @author James Moger
 *
 */
public class FederationUtils {

	private static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
	}.getType();

	private static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
	}.getType();

	private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
	}.getType();

	private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
	}.getType();

	private static final Logger LOGGER = LoggerFactory.getLogger(FederationUtils.class);

	/**
	 * Returns an url to this servlet for the specified parameters.
	 *
	 * @param sourceURL
	 *            the url of the source gitblit instance
	 * @param token
	 *            the federation token of the source gitblit instance
	 * @param req
	 *            the pull type request
	 */
	public static String asLink(String sourceURL, String token, FederationRequest req) {
		return asLink(sourceURL, null, token, req, null);
	}

	/**
	 *
	 * @param remoteURL
	 *            the url of the remote gitblit instance
	 * @param tokenType
	 *            the type of federation token of a gitblit instance
	 * @param token
	 *            the federation token of a gitblit instance
	 * @param req
	 *            the pull type request
	 * @param myURL
	 *            the url of this gitblit instance
	 * @return
	 */
	public static String asLink(String remoteURL, FederationToken tokenType, String token,
			FederationRequest req, String myURL) {
		if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
			remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
		}
		if (req == null) {
			req = FederationRequest.PULL_REPOSITORIES;
		}
		return remoteURL + Constants.FEDERATION_PATH + "?req=" + req.name().toLowerCase()
				+ (token == null ? "" : ("&token=" + token))
				+ (tokenType == null ? "" : ("&tokenType=" + tokenType.name().toLowerCase()))
				+ (myURL == null ? "" : ("&url=" + StringUtils.encodeURL(myURL)));
	}

	/**
	 * Returns the list of federated gitblit instances that this instance will
	 * try to pull.
	 *
	 * @return list of registered gitblit instances
	 */
	public static List<FederationModel> getFederationRegistrations(IStoredSettings settings) {
		List<FederationModel> federationRegistrations = new ArrayList<FederationModel>();
		List<String> keys = settings.getAllKeys(Keys.federation._ROOT);
		keys.remove(Keys.federation.name);
		keys.remove(Keys.federation.passphrase);
		keys.remove(Keys.federation.allowProposals);
		keys.remove(Keys.federation.proposalsFolder);
		keys.remove(Keys.federation.defaultFrequency);
		keys.remove(Keys.federation.sets);
		Collections.sort(keys);
		Map<String, FederationModel> federatedModels = new HashMap<String, FederationModel>();
		for (String key : keys) {
			String value = key.substring(Keys.federation._ROOT.length() + 1);
			List<String> values = StringUtils.getStringsFromValue(value, "\\.");
			String server = values.get(0);
			if (!federatedModels.containsKey(server)) {
				federatedModels.put(server, new FederationModel(server));
			}
			String setting = values.get(1);
			if (setting.equals("url")) {
				// url of the origin Gitblit instance
				federatedModels.get(server).url = settings.getString(key, "");
			} else if (setting.equals("token")) {
				// token for the origin Gitblit instance
				federatedModels.get(server).token = settings.getString(key, "");
			} else if (setting.equals("frequency")) {
				// frequency of the pull operation
				federatedModels.get(server).frequency = settings.getString(key, "");
			} else if (setting.equals("folder")) {
				// destination folder of the pull operation
				federatedModels.get(server).folder = settings.getString(key, "");
			} else if (setting.equals("bare")) {
				// whether pulled repositories should be bare
				federatedModels.get(server).bare = settings.getBoolean(key, true);
			} else if (setting.equals("mirror")) {
				// are the repositories to be true mirrors of the origin
				federatedModels.get(server).mirror = settings.getBoolean(key, true);
			} else if (setting.equals("mergeAccounts")) {
				// merge remote accounts into local accounts
				federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false);
			} else if (setting.equals("sendStatus")) {
				// send a status acknowledgment to source Gitblit instance
				// at end of git pull
				federatedModels.get(server).sendStatus = settings.getBoolean(key, false);
			} else if (setting.equals("notifyOnError")) {
				// notify administrators on federation pull failures
				federatedModels.get(server).notifyOnError = settings.getBoolean(key, false);
			} else if (setting.equals("exclude")) {
				// excluded repositories
				federatedModels.get(server).exclusions = settings.getStrings(key);
			} else if (setting.equals("include")) {
				// included repositories
				federatedModels.get(server).inclusions = settings.getStrings(key);
			}
		}

		// verify that registrations have a url and a token
		for (FederationModel model : federatedModels.values()) {
			if (StringUtils.isEmpty(model.url)) {
				LOGGER.warn(MessageFormat.format(
						"Dropping federation registration {0}. Missing url.", model.name));
				continue;
			}
			if (StringUtils.isEmpty(model.token)) {
				LOGGER.warn(MessageFormat.format(
						"Dropping federation registration {0}. Missing token.", model.name));
				continue;
			}
			// set default frequency if unspecified
			if (StringUtils.isEmpty(model.frequency)) {
				model.frequency = settings.getString(Keys.federation.defaultFrequency, "60 mins");
			}
			federationRegistrations.add(model);
		}
		return federationRegistrations;
	}

	/**
	 * Sends a federation poke to the Gitblit instance at remoteUrl. Pokes are
	 * sent by an pulling Gitblit instance to an origin Gitblit instance as part
	 * of the proposal process. This is to ensure that the pulling Gitblit
	 * instance has an IP route to the origin instance.
	 *
	 * @param remoteUrl
	 *            the remote Gitblit instance to send a federation proposal to
	 * @param proposal
	 *            a complete federation proposal
	 * @return true if there is a route to the remoteUrl
	 */
	public static boolean poke(String remoteUrl) throws Exception {
		String url = asLink(remoteUrl, null, FederationRequest.POKE);
		String json = JsonUtils.toJsonString("POKE");
		int status = JsonUtils.sendJsonString(url, json);
		return status == HttpServletResponse.SC_OK;
	}

	/**
	 * Sends a federation proposal to the Gitblit instance at remoteUrl
	 *
	 * @param remoteUrl
	 *            the remote Gitblit instance to send a federation proposal to
	 * @param proposal
	 *            a complete federation proposal
	 * @return the federation proposal result code
	 */
	public static FederationProposalResult propose(String remoteUrl, FederationProposal proposal)
			throws Exception {
		String url = asLink(remoteUrl, null, FederationRequest.PROPOSAL);
		String json = JsonUtils.toJsonString(proposal);
		int status = JsonUtils.sendJsonString(url, json);
		switch (status) {
		case HttpServletResponse.SC_FORBIDDEN:
			// remote Gitblit Federation disabled
			return FederationProposalResult.FEDERATION_DISABLED;
		case HttpServletResponse.SC_BAD_REQUEST:
			// remote Gitblit did not receive any JSON data
			return FederationProposalResult.MISSING_DATA;
		case HttpServletResponse.SC_METHOD_NOT_ALLOWED:
			// remote Gitblit not accepting proposals
			return FederationProposalResult.NO_PROPOSALS;
		case HttpServletResponse.SC_NOT_ACCEPTABLE:
			// remote Gitblit failed to poke this Gitblit instance
			return FederationProposalResult.NO_POKE;
		case HttpServletResponse.SC_OK:
			// received
			return FederationProposalResult.ACCEPTED;
		default:
			return FederationProposalResult.ERROR;
		}
	}

	/**
	 * Retrieves a map of the repositories at the remote gitblit instance keyed
	 * by the repository clone url.
	 *
	 * @param registration
	 * @param checkExclusions
	 *            should returned repositories remove registration exclusions
	 * @return a map of cloneable repositories
	 * @throws Exception
	 */
	public static Map<String, RepositoryModel> getRepositories(FederationModel registration,
			boolean checkExclusions) throws Exception {
		String url = asLink(registration.url, registration.token,
				FederationRequest.PULL_REPOSITORIES);
		Map<String, RepositoryModel> models = JsonUtils.retrieveJson(url, REPOSITORIES_TYPE);
		if (checkExclusions) {
			Map<String, RepositoryModel> includedModels = new HashMap<String, RepositoryModel>();
			for (Map.Entry<String, RepositoryModel> entry : models.entrySet()) {
				if (registration.isIncluded(entry.getValue())) {
					includedModels.put(entry.getKey(), entry.getValue());
				}
			}
			return includedModels;
		}
		return models;
	}

	/**
	 * Tries to pull the gitblit user accounts from the remote gitblit instance.
	 *
	 * @param registration
	 * @return a collection of UserModel objects
	 * @throws Exception
	 */
	public static List<UserModel> getUsers(FederationModel registration) throws Exception {
		String url = asLink(registration.url, registration.token, FederationRequest.PULL_USERS);
		Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE);
		List<UserModel> list = new ArrayList<UserModel>(models);
		return list;
	}

	/**
	 * Tries to pull the gitblit team definitions from the remote gitblit
	 * instance.
	 *
	 * @param registration
	 * @return a collection of TeamModel objects
	 * @throws Exception
	 */
	public static List<TeamModel> getTeams(FederationModel registration) throws Exception {
		String url = asLink(registration.url, registration.token, FederationRequest.PULL_TEAMS);
		Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE);
		List<TeamModel> list = new ArrayList<TeamModel>(models);
		return list;
	}

	/**
	 * Tries to pull the gitblit server settings from the remote gitblit
	 * instance.
	 *
	 * @param registration
	 * @return a map of the remote gitblit settings
	 * @throws Exception
	 */
	public static Map<String, String> getSettings(FederationModel registration) throws Exception {
		String url = asLink(registration.url, registration.token, FederationRequest.PULL_SETTINGS);
		Map<String, String> settings = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
		return settings;
	}

	/**
	 * Tries to pull the referenced scripts from the remote gitblit instance.
	 *
	 * @param registration
	 * @return a map of the remote gitblit scripts by script name
	 * @throws Exception
	 */
	public static Map<String, String> getScripts(FederationModel registration) throws Exception {
		String url = asLink(registration.url, registration.token, FederationRequest.PULL_SCRIPTS);
		Map<String, String> scripts = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
		return scripts;
	}

	/**
	 * Send an status acknowledgment to the remote Gitblit server.
	 *
	 * @param identification
	 *            identification of this pulling instance
	 * @param registration
	 *            the source Gitblit instance to receive an acknowledgment
	 * @param results
	 *            the results of your pull operation
	 * @return true, if the remote Gitblit instance acknowledged your results
	 * @throws Exception
	 */
	public static boolean acknowledgeStatus(String identification, FederationModel registration)
			throws Exception {
		String url = asLink(registration.url, null, registration.token, FederationRequest.STATUS,
				identification);
		String json = JsonUtils.toJsonString(registration);
		int status = JsonUtils.sendJsonString(url, json);
		return status == HttpServletResponse.SC_OK;
	}
}
