package {{invokerPackage}};

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import {{invokerPackage}}.auth.ApiKeyAuth;
import {{invokerPackage}}.auth.HttpBasicAuth;
import com.perforce.hwsclient.models.HWSStatus;
import com.perforce.hwsclient.models.LoginResponse;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import retrofit.ErrorHandler;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.client.OkClient;
import retrofit.converter.ConversionException;
import retrofit.converter.Converter;
import retrofit.converter.GsonConverter;
import retrofit.mime.TypedByteArray;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;

/**
 * The Java client which fronts the HelixWebServices REST api.
 * Note that this is generated from a mustache template and
 * should not be edited directly.
 */
public class ApiClient {

	/** Map of authorized accessors. */
	private Map<String, Interceptor> apiAuthorizations;
	/** Http request processor. */
	private OkHttpClient okClient;
	/** Rest adapter factory. */
	private RestAdapter.Builder adapterBuilder;

	/** Static /api path for requests. */
	private static final String API_PATH = "/api";
	/** Constant current version. */
	private static final String SUPPORTED_VERSION = "v16.1";
	/** Top level path for requests. */
	private String basePath;

	/**
	 * Apply configuration header overrides to our requests.
	 * <p>
	 * This method will provide the typical prefix to each configuration option,
	 * so keys in this map should just be the configuration value name. For
	 * example, <code>P4PORT</code>.
	 *
	 * @param overrides
	 *            A map of configuration option to configuration value.
	 */
	public void addOverrides(final Map<String, String> overrides) {
		if (overrides != null && !overrides.isEmpty()) {
			adapterBuilder.setRequestInterceptor(request -> {
				overrides.entrySet().forEach(e -> {
					request.addHeader(
							"X-Perforce-Helix-Web-Services-" + e.getKey(),
							e.getValue());
				});
			});
		}
	}

	/**
	 * The base URL to your HWS instance.
	 *
	 * @return The root to your HWS server, e.g.,
	 *         https://perforce.mycompany.com/hws
	 */
	public String getBasePath() {
		return basePath;
	}

	/**
	 * Change the base URL to the HWS instance.
	 *
	 * Changes will not be reflected until createDefaultAdapter is called again.
	 *
	 * @param basePath
	 *            The HWS root URL
	 */
	public void setBasePath(final String basePath) {
		this.basePath = basePath;
	}

	/**
	 * A special construction option that allows your client to trust all
	 * certificates.
	 *
	 * By default, all HWS instances start with a self-signed certificate. You
	 * may need to construct using this variation to validate your code is
	 * working in a non-production system.
	 *
	 * @param trustAllSsl
	 *            When true, we do not validate SSL.
	 * @param basePath
	 *            The HWS root URL
	 */
	public ApiClient(final boolean trustAllSsl, final String basePath) {
		this.basePath = basePath;
		apiAuthorizations = new LinkedHashMap<>();

		createDefaultAdapter(trustAllSsl);
	}

	/**
	 * Construct an ApiClient with the indicated basePath.
	 *
	 * If you are testing out a new installation, this will likely not work due
	 * to the use of a self-signed certificate on the server.
	 *
	 * @param basePath
	 *            The HWS root URL
	 */
	public ApiClient(final String basePath) {
		this(false, basePath);
	}

	/**
	 * Construct an ApiClient, establishing a login session with the "project"
	 * login server.
	 *
	 * This will generate an ApiClient instance, and, establish the security
	 * header by calling the <code>POST /projects/v1/login</code> method.
	 *
	 * @param basePath
	 *            The HWS root URL
	 * @param username
	 *            The Perforce user to connect as
	 * @param password
	 *            The Perforce password to authenticate with (discarded after
	 *            ticket is acquired)
	 * @return The ApiClient handle ready to make authenticated requests.
	 */
	public static ApiClient createWithTicket(
			final String basePath, final String username,
			final String password) {
		return createWithTicket(false, basePath, username, password);
	}
	
	/**
	 * Construct an ApiClient, establishing a login session with the "project"
	 * login server.
	 *
	 * This will generate an ApiClient instance, and, establish the security
	 * header by calling the <code>POST /projects/v1/login</code> method.
	 *
	 * @param trustAllSsl
	 *            When true, we do not validate SSL.
	 * @param basePath
	 *            The HWS root URL
	 * @param username
	 *            The Perforce user to connect as
	 * @param password
	 *            The Perforce password to authenticate with (discarded after
	 *            ticket is acquired)
	 * @return The ApiClient handle ready to make authenticated requests.
	 */
	public static ApiClient createWithTicket(
			final boolean trustAllSsl,
			final String basePath, final String username,
			final String password) {
		ApiClient apiClient = new ApiClient(trustAllSsl, basePath);
		apiClient.setCredentials(username, password);
		DefaultApi api = apiClient.createDefaultService();
		LoginResponse loginResponse = api.login();
		apiClient.setApiKey(loginResponse.getTicket());
		return apiClient;
	}

	/**
	 * Checks status of the API against the currently configured server.
	 *
	 * @return true if the server is not currently reporting any problems.
	 */
	public boolean isOK() {
		try {
			HWSStatus status = getStatus();
			return status != null && "OK".equals(status.getStatus());
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Gets the status.
	 *
	 * @return the status
	 */
	public HWSStatus getStatus() {
		return createDefaultService().getStatus();
	}

	/**
	 * Will do a custom fetch of the URL at "/api", and will see if our
	 * API_PATH string pops up in the mix.
	 *
	 * @return False if the server does not tell us we're a valid version.
	 *         (Might happen if you can't access the server too.)
	 */
	public boolean isSupported() {
		try {
			Request request = new Request.Builder().url(basePath
					+ "/api/hws/version").method("GET", null)
					.addHeader("Accept", "application/json").build();
			Call call = okClient.newCall(request);
			Response response = call.execute();
			Gson gson = new Gson();
			Type mapType = new TypeToken<Map<String, Object>>() {
			}.getType();
			Map<String, Object> info = gson.fromJson(response.body().string(),
					mapType);
			@SuppressWarnings("unchecked")
			List<String> versions = (List<String>) info.get("supportedVersions");
			return versions.stream().anyMatch(v -> SUPPORTED_VERSION.equals(v));
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Initializes the adapterBuilder, typically called during construction.
	 *
	 * @param trustAllSsl
	 *            when true will configure the adapter to trust all connections.
	 */
	public void createDefaultAdapter(final boolean trustAllSsl) {
		Gson gson = new GsonBuilder()
				.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create();

		okClient = new OkHttpClient();

		if (trustAllSsl) {
			// Create a trust manager that does not validate certificate chains
			final TrustManager[] trustAllCerts = new TrustManager[] {
			         new X509TrustManager() {
				@Override
				public void checkClientTrusted(
						final java.security.cert.X509Certificate[] chain,
						final String authType)
						throws CertificateException {
				}

				@Override
				public void checkServerTrusted(
						final java.security.cert.X509Certificate[] chain,
						final String authType)
						throws CertificateException {
				}

				@Override
				public java.security.cert.X509Certificate[] getAcceptedIssuers() {
					return new java.security.cert.X509Certificate[] {};
				}
			} };

			// Install the all-trusting trust manager
			final SSLContext sslContext;
			try {
				sslContext = SSLContext.getInstance("SSL");
				sslContext.init(null, trustAllCerts,
						new java.security.SecureRandom());
			} catch (KeyManagementException | NoSuchAlgorithmException e) {
				throw new RuntimeException(e);
			}

			// Create an ssl socket factory with our all-trusting manager
			final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

			okClient.setSslSocketFactory(sslSocketFactory);
			okClient.setHostnameVerifier((hostname, session) -> true);
		}

		adapterBuilder = new RestAdapter.Builder().setEndpoint(basePath + API_PATH)
				.setClient(new OkClient(okClient))
				.setErrorHandler(new ErrorHandler() {
					
					@Override
					public Throwable handleError(final RetrofitError arg0) {
						return new HwsRestException(arg0);
					}
				})
				.setConverter(new GsonConverterWrapper(gson));
	}

	/**
	 * Obtain a handle to the service object to interact with HWS.
	 *
	 * @return The primary API object to interact with HWS.
	 */
	public DefaultApi createDefaultService() {
		return createService(DefaultApi.class);
	}

	/**
	 * Create a service object to interact with the server.
	 *
	 * In general, you'll probably just want the DefaultApi here.
	 *
	 * @param <S>
	 *            The interface API (probably just DefaultApi.class)
	 * @param serviceClass
	 *            The interface class indicating the API you want to use.
	 * @return The interface object to interact with the remote server.
	 */
	public <S> S createService(final Class<S> serviceClass) {
		return adapterBuilder.build().create(serviceClass);
	}

	/**
	 * Helper method to configure the username/password for basic auth or
	 * password oauth.
	 * 
	 * @param username
	 *            The Perforce login
	 * @param ticket
	 *            The Perforce ticket (not the user's password)
	 */
	public void setCredentials(final String username, final String ticket) {
		HttpBasicAuth auth = new HttpBasicAuth();
		auth.setUsername(username);
		auth.setPassword(ticket);
		updateAuthorization("ticket_auth", auth);
	}

	/**
	 * Helper method to set the API key as the "ticket_auth" scheme.
	 * 
	 * @param apiKey the token to set for authorization
	 */
	public void setApiKey(final String apiKey) {
		ApiKeyAuth auth = new ApiKeyAuth("header", "Authorization");
		auth.setApiKey(apiKey);
		updateAuthorization("ticket_auth", auth);
	}

	/**
	 * Add an authorization token.
	 * @param authName the name associated with a token
	 * @param authorization the token
	 */
	private void updateAuthorization(final String authName,
			final Interceptor authorization) {
		okClient.interceptors().clear();
		okClient.interceptors().add(authorization);
		apiAuthorizations.put(authName, authorization);
	}

	/**
	 * Get the adapter builder.
	 * @return the current builder of adapters.
	 */
	public RestAdapter.Builder getAdapterBuilder() {
		return adapterBuilder;
	}

	/**
	 * Set a new adapter builder.
	 * @param adapterBuilder the new adapter builder to use
	 */
	public void setAdapterBuilder(final RestAdapter.Builder adapterBuilder) {
		this.adapterBuilder = adapterBuilder;
	}

	/**
	 * Get the http client.
	 * @return the current http client
	 */
	public OkHttpClient getOkClient() {
		return okClient;
	}

	/**
	 * Set the current authorizations in the http client.
	 * @param okClient the http client to be modified.
	 */
	public void addAuthsToOkClient(final OkHttpClient okClient) {
		for (Interceptor apiAuthorization : apiAuthorizations.values()) {
			okClient.interceptors().add(apiAuthorization);
		}
	}

	/**
	 * Clones the okClient given in parameter, adds the auth interceptors and
	 * uses it to configure the RestAdapter.
	 * 
	 * @param okClient
	 *            The OKHttpClient to clone
	 */
	public void configureFromOkclient(final OkHttpClient okClient) {
		OkHttpClient clone = okClient.clone();
		addAuthsToOkClient(clone);
		adapterBuilder.setClient(new OkClient(clone));
	}
	
		
	/**
	 * Wrapper class for the retrofit error to indicate that it was
	 * handled by a perforce api.
	 */
	public class HwsRestException extends RuntimeException {
		/** Generated id, not expected to change. */
		private static final long serialVersionUID = -8177702544598405834L;
		/** The original retrofit error. */
		private final RetrofitError retrofitError;
		/**
		 * Instantiate a new exception to wrap the retrofit error.
		 * @param retrofitError the original rest error
		 */
		HwsRestException(final RetrofitError retrofitError) {
			super(retrofitError.getLocalizedMessage());
			this.retrofitError = retrofitError;
		}
		/**
		 * Get the original retrofit error, for further analysis.
		 * @return RetrofitError the original error
		 */
		public RetrofitError getRetrofitError() {
			return retrofitError;
		}
	}
}

/**
 * This wrapper is to take care of this case: when the deserialization fails due
 * to JsonParseException and the expected type is String, then just return the
 * body string.
 */
class GsonConverterWrapper implements Converter {

	/** The actual converter instance. */
	private GsonConverter converter;

	/** 
	 * Constructor with a converter instance.
	 * @param gson the java <-> json converter
	 */
	GsonConverterWrapper(final Gson gson) {
		converter = new GsonConverter(gson);
	}

	@Override
	public Object fromBody(final TypedInput body, final Type type)
			throws ConversionException {
		byte[] bodyBytes = readInBytes(body);
		TypedByteArray newBody = new TypedByteArray(body.mimeType(), bodyBytes);
		try {
			return converter.fromBody(newBody, type);
		} catch (ConversionException e) {
			if (e.getCause() instanceof JsonParseException
					&& type.equals(String.class)) {
				return new String(bodyBytes);
			} else {
				throw e;
			}
		}
	}

	@Override
	public TypedOutput toBody(final Object object) {
		return converter.toBody(object);
	}

	/**
	 * Read a content body one byte at a time.
	 * @param body the content
	 * @return a byte array equivalent of the content
	 * @throws ConversionException if it cannot be converted
	 */
	private byte[] readInBytes(final TypedInput body)
			throws ConversionException {
		InputStream in = null;
		try {
			in = body.in();
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			byte[] buffer = new byte[0xFFFF];
			for (int len; (len = in.read(buffer)) != -1;) {
				os.write(buffer, 0, len);
			}
			os.flush();
			return os.toByteArray();
		} catch (IOException e) {
			throw new ConversionException(e);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException ignored) {
				}
			}
		}
	}
}