package com.docusign.esign.client.auth;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.ws.rs.core.Response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.AuthenticationRequestBuilder;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.apache.oltu.oauth2.common.token.BasicOAuthToken;

import com.docusign.esign.client.Pair;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;

import io.swagger.annotations.ApiModelProperty;


public class OAuth implements Authentication {
	static final int MILLIS_PER_SECOND = 1000;

	// OAuth Scope constants
	/** create and send envelopes, and obtain links for starting signing sessions. */
	public final static String Scope_SIGNATURE = "signature";
	/** obtain a refresh token with an extended lifetime. */
	public final static String Scope_EXTENDED = "extended";
	/** obtain access to the user’s account when the user is not present. */
	public final static String Scope_IMPERSONATION = "impersonation";

	// OAuth ResponseType constants
	/** used by public/native client applications. */
	public final static String CODE = "code";
	/** used by private/trusted client application. */
	public final static String TOKEN = "token";

	// OAuth base path constants
	/** live/production base path */
	public final static String PRODUCTION_OAUTH_BASEPATH = "account.docusign.com";
	/** sandbox/demo base path */
	public final static String DEMO_OAUTH_BASEPATH = "account-d.docusign.com";
	/** stage base path */
	public final static String STAGE_OAUTH_BASEPATH = "account-s.docusign.com";

	// OAuth grant types
	/** JWT grant type */
	public final static String GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer";

	private volatile String accessToken;
	private Long expirationTimeMillis;
	private OAuthClient oauthClient;
	private TokenRequestBuilder tokenRequestBuilder;
	private AuthenticationRequestBuilder authenticationRequestBuilder;
	private AccessTokenListener accessTokenListener;

	public OAuth() {
		this(null, null, null);
	}

	public OAuth(Client client, TokenRequestBuilder tokenRequestBuilder, AuthenticationRequestBuilder authenticationRequestBuilder) {
		this.oauthClient = new OAuthClient(new OAuthJerseyClient(client));
		this.tokenRequestBuilder = tokenRequestBuilder;
		this.authenticationRequestBuilder = authenticationRequestBuilder;
	}

	public OAuth(Client client, OAuthFlow flow, String authorizationUrl, String tokenUrl, String scopes) {
		this(client, OAuthClientRequest.tokenLocation(tokenUrl).setScope(scopes), OAuthClientRequest.authorizationLocation(authorizationUrl).setScope(scopes));

		switch (flow) {
		case accessCode:
			tokenRequestBuilder.setGrantType(GrantType.AUTHORIZATION_CODE);
			authenticationRequestBuilder.setResponseType(OAuth.CODE);
			break;
		case implicit:
			tokenRequestBuilder.setGrantType(GrantType.IMPLICIT);
			authenticationRequestBuilder.setResponseType(OAuth.TOKEN);
			break;
		case password:
			tokenRequestBuilder.setGrantType(GrantType.PASSWORD);
			break;
		case application:
			tokenRequestBuilder.setGrantType(GrantType.CLIENT_CREDENTIALS);
			break;
		default:
			break;
		}
	}

	public OAuth(OAuthFlow flow, String authorizationUrl, String tokenUrl, String scopes) {
		this(new Client(null, null), flow, authorizationUrl, tokenUrl, scopes);
	}

	@Override
	public void applyToParams(List<Pair> queryParams, Map<String, String> headerParams) {
		// If first time, get the token
		if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) {
			updateAccessToken();
		}
		if (accessToken != null) {
			headerParams.put("Authorization", "Bearer " + accessToken);
		}
	}

	public synchronized void updateAccessToken() {
		OAuthJSONAccessTokenResponse accessTokenResponse;
		try {
			accessTokenResponse = oauthClient.accessToken(tokenRequestBuilder.buildBodyMessage());
		} catch (Exception e) {
			throw new ClientHandlerException(e.getMessage(), e);
		}
		if (accessTokenResponse != null)
		{
			// FIXME: This does not work in case of non HTTP 200 :-( oauthClient needs to return the plain HTTP resonse
			if (accessTokenResponse.getResponseCode() != Response.Status.OK.getStatusCode())
			{
				throw new ClientHandlerException("Error while requesting an access token, received HTTP code: " + accessTokenResponse.getResponseCode());
			}

			if (accessTokenResponse.getAccessToken() == null) {
				throw new ClientHandlerException("Error while requesting an access token. No 'access_token' found.");
			}
			if (accessTokenResponse.getExpiresIn() == null) {
				throw new ClientHandlerException("Error while requesting an access token. No 'expires_in' found.");
			}

			setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getExpiresIn());
			if (this.accessTokenListener != null) {
				this.accessTokenListener.notify((BasicOAuthToken)accessTokenResponse.getOAuthToken());
			}
		} else {
			// in case of HTTP error codes accessTokenResponse is null, thus no check of accessTokenResponse.getResponseCode() possible :-(
			throw new ClientHandlerException("Error while requesting an access token. No accessTokenResponse object recieved, maybe a non HTTP 200 received?");
		}
	}

	public synchronized void registerAccessTokenListener(AccessTokenListener accessTokenListener) {
		this.accessTokenListener = accessTokenListener;
	}

	public synchronized String getAccessToken() {
		return accessToken;
	}

	public synchronized void setAccessToken(String accessToken, Long expiresIn) {
		this.accessToken = accessToken;
		this.expirationTimeMillis = System.currentTimeMillis() + expiresIn * MILLIS_PER_SECOND;
	}

	public TokenRequestBuilder getTokenRequestBuilder() {
		return tokenRequestBuilder;
	}

	public void setTokenRequestBuilder(TokenRequestBuilder tokenRequestBuilder) {
		this.tokenRequestBuilder = tokenRequestBuilder;
	}

	public AuthenticationRequestBuilder getAuthenticationRequestBuilder() {
		return authenticationRequestBuilder;
	}

	public void setAuthenticationRequestBuilder(AuthenticationRequestBuilder authenticationRequestBuilder) {
		this.authenticationRequestBuilder = authenticationRequestBuilder;
	}

	public OAuthClient getOauthClient() {
		return oauthClient;
	}

	public void setOauthClient(OAuthClient oauthClient) {
		this.oauthClient = oauthClient;
	}

	public void setOauthClient(Client client) {
		this.oauthClient = new OAuthClient(new OAuthJerseyClient(client));
	}

	/**
	 *
	 * OAuthToken model with the following properties:
	 * <br><b>accessToken</b>: the token you will use in the Authorization header of calls to the DocuSign API.
	 * <br><b>tokenType</b>: this is the type of the accessToken. It is usually "Bearer".
	 * <br><b>refreshToken</b>: a token you can use to get a new accessToken without requiring user interaction.
	 * <br><b>expiresIn</b>: the number of seconds before the accessToken expires.
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class OAuthToken {
		@JsonProperty("access_token")
		private String accessToken = null;

		@JsonProperty("token_type")
		private String tokenType = null;

		@JsonProperty("refresh_token")
		private String refreshToken = null;

		@JsonProperty("expires_in")
		private Long expiresIn = 0L;

		public OAuthToken accessToken(String accessToken) {
			this.accessToken = accessToken;
			return this;
		}

		/**
		 * Get accessToken
		 *
		 * @return accessToken
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getAccessToken() {
			return accessToken;
		}

		public void setAccessToken(String accessToken) {
			this.accessToken = accessToken;
		}

		public OAuthToken isTokenType(String tokenType) {
			this.tokenType = tokenType;
			return this;
		}

		/**
		 * Get tokenType
		 *
		 * @return tokenType
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getTokenType() {
			return tokenType;
		}

		public void setTokenType(String tokenType) {
			this.tokenType = tokenType;
		}

		public OAuthToken refreshToken(String refreshToken) {
			this.refreshToken = refreshToken;
			return this;
		}

		/**
		 * Get refreshToken
		 *
		 * @return refreshToken
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getRefreshToken() {
			return refreshToken;
		}

		public void setRefreshToken(String refreshToken) {
			this.refreshToken = refreshToken;
		}

		public OAuthToken expiresIn(Long expiresIn) {
			this.expiresIn = expiresIn;
			return this;
		}

		/**
		 * Get expiresIn
		 *
		 * @return expiresIn
		 **/
		@ApiModelProperty(example = "3600L", value = "0L")
		public Long getExpiresIn() {
			return expiresIn;
		}

		public void setExpiresIn(Long expiresIn) {
			this.expiresIn = expiresIn;
		}

		@Override
		public boolean equals(java.lang.Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			OAuthToken oAuthToken = (OAuthToken) o;
			return Objects.equals(this.accessToken, oAuthToken.accessToken)
					&& Objects.equals(this.tokenType, oAuthToken.tokenType)
					&& Objects.equals(this.refreshToken, oAuthToken.refreshToken)
					&& Objects.equals(this.expiresIn, oAuthToken.expiresIn);
		}

		@Override
		public int hashCode() {
			return Objects.hash(accessToken, tokenType, refreshToken, expiresIn);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("class OAuthToken {\n");

			sb.append("    accessToken: ").append(toIndentedString(accessToken)).append("\n");
			sb.append("    tokenType: ").append(toIndentedString(tokenType)).append("\n");
			sb.append("    refreshToken: ").append(toIndentedString(refreshToken)).append("\n");
			sb.append("    expiresIn: ").append(toIndentedString(expiresIn)).append("\n");
			return sb.toString();
		}

		/**
		 * Convert the given object to string with each line indented by 4
		 * spaces (except the first line).
		 */
		private String toIndentedString(java.lang.Object o) {
			if (o == null) {
				return "null";
			}
			return o.toString().replace("\n", "\n    ");
		}
	}

	/**
	 *
	 * Link model with the following properties:
	 * <br><b>rel</b>: currently the only value is "self".
	 * <br><b>href</b>: the direct link of the organization.
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class Link {
		@JsonProperty("rel")
		private String rel = null;

		@JsonProperty("href")
		private String href = null;

		public Link rel(String rel) {
			this.rel = rel;
			return this;
		}

		/**
		 * Get rel
		 *
		 * @return rel
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getRel() {
			return rel;
		}

		public void setRel(String rel) {
			this.rel = rel;
		}

		public Link href(String href) {
			this.href = href;
			return this;
		}

		/**
		 * Get href
		 *
		 * @return href
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getHref() {
			return href;
		}

		public void setHref(String href) {
			this.href = href;
		}

		@Override
		public boolean equals(java.lang.Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			Link link = (Link) o;
			return Objects.equals(this.rel, link.rel)
					&& Objects.equals(this.href, link.href);
		}

		@Override
		public int hashCode() {
			return Objects.hash(rel, href);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("class Link {\n");

			sb.append("    rel: ").append(toIndentedString(rel)).append("\n");
			sb.append("    href: ").append(toIndentedString(href)).append("\n");
			return sb.toString();
		}

		/**
		 * Convert the given object to string with each line indented by 4
		 * spaces (except the first line).
		 */
		private String toIndentedString(java.lang.Object o) {
			if (o == null) {
				return "null";
			}
			return o.toString().replace("\n", "\n    ");
		}
	}

	/**
	 *
	 * Organization model with the following properties:
	 * <br><b>organizationId</b>: the organization ID GUID if DocuSign Org Admin is enabled.
	 * <br><b>links</b>: this is list of organization direct links associated with the DocuSign account.
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class Organization {
		@JsonProperty("organization_id")
		private String organizationId = null;

		@JsonProperty("links")
		private java.util.List<Link> links = new java.util.ArrayList<Link>();

		public Organization organizationId(String organizationId) {
			this.organizationId = organizationId;
			return this;
		}

		/**
		 * Get organizationId
		 *
		 * @return organizationId
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getOrganizationId() {
			return organizationId;
		}

		public void setOrganizationId(String organizationId) {
			this.organizationId = organizationId;
		}

		public Organization links(java.util.List<Link> links) {
			this.links = links;
			return this;
		}

		public Organization addLinksItem(Link linksItem) {
			this.links.add(linksItem);
			return this;
		}

		/**
		 * Get links
		 *
		 * @return links
		 **/
		@ApiModelProperty(example = "null", value = "")
		public java.util.List<Link> getLinks() {
			return links;
		}

		public void setLinks(java.util.List<Link> links) {
			this.links = links;
		}

		@Override
		public boolean equals(java.lang.Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			Organization organization = (Organization) o;
			return Objects.equals(this.organizationId, organization.organizationId)
					&& Objects.equals(this.links, organization.links);
		}

		@Override
		public int hashCode() {
			return Objects.hash(organizationId, links);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("class Organization {\n");

			sb.append("    organizationId: ").append(toIndentedString(organizationId)).append("\n");
			sb.append("    links: ").append(toIndentedString(links)).append("\n");
			return sb.toString();
		}

		/**
		 * Convert the given object to string with each line indented by 4
		 * spaces (except the first line).
		 */
		private String toIndentedString(java.lang.Object o) {
			if (o == null) {
				return "null";
			}
			return o.toString().replace("\n", "\n    ");
		}
	}

	/**
	 *
	 * Account model with the following properties:
	 * <br><b>accountId</b>: the account ID GUID.
	 * <br><b>isDefault</b>: whether this is the default account, when the user has access to multiple accounts.
	 * <br><b>accountName</b>: the human-readable name of the account.
	 * <br><b>baseUri</b>: the base URI associated with this account.
	 * It also tells which DocuSign data center the account is hosted on.
	 * <br><b>organization</b>: If DocuSign Org Admin is enabled on this account,
	 * this property contains the organization information.
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class Account {
		@JsonProperty("account_id")
		private String accountId = null;

		@JsonProperty("is_default")
		private String isDefault = null;

		@JsonProperty("account_name")
		private String accountName = null;

		@JsonProperty("base_uri")
		private String baseUri = null;

		@JsonProperty("organization")
		private Organization organization = new Organization();

		public Account accountId(String accountId) {
			this.accountId = accountId;
			return this;
		}

		/**
		 * Get accountId
		 *
		 * @return accountId
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getAccountId() {
			return accountId;
		}

		public void setAccountId(String accountId) {
			this.accountId = accountId;
		}

		public Account isDefault(String isDefault) {
			this.isDefault = isDefault;
			return this;
		}

		/**
		 * Get isDefault
		 *
		 * @return isDefault
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getIsDefault() {
			return isDefault;
		}

		public void setIsDefault(String isDefault) {
			this.isDefault = isDefault;
		}

		public Account accountName(String accountName) {
			this.accountName = accountName;
			return this;
		}

		/**
		 * Get accountName
		 *
		 * @return accountName
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getAccountName() {
			return accountName;
		}

		public void setAccountName(String accountName) {
			this.accountName = accountName;
		}

		public Account baseUri(String baseUri) {
			this.baseUri = baseUri;
			return this;
		}

		/**
		 * Get baseUri
		 *
		 * @return baseUri
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getBaseUri() {
			return baseUri;
		}

		public void setBaseUri(String baseUri) {
			this.baseUri = baseUri;
		}

		public Account organization(Organization organization) {
			this.organization = organization;
			return this;
		}

		/**
		 * Get organization
		 *
		 * @return organization
		 **/
		@ApiModelProperty(example = "null", value = "")
		public Organization getOrganization() {
			return organization;
		}

		public void setOrganization(Organization organization) {
			this.organization = organization;
		}

		@Override
		public boolean equals(java.lang.Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			Account account = (Account) o;
			return Objects.equals(this.accountId, account.accountId)
					&& Objects.equals(this.isDefault, account.isDefault)
					&& Objects.equals(this.accountName, account.accountName)
					&& Objects.equals(this.baseUri, account.baseUri)
					&& Objects.equals(this.organization, account.organization);
		}

		@Override
		public int hashCode() {
			return Objects.hash(accountId, isDefault, accountName, baseUri, organization);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("class Account {\n");

			sb.append("    accountId: ").append(toIndentedString(accountId)).append("\n");
			sb.append("    isDefault: ").append(toIndentedString(isDefault)).append("\n");
			sb.append("    accountName: ").append(toIndentedString(accountName)).append("\n");
			sb.append("    baseUri: ").append(toIndentedString(baseUri)).append("\n");
			sb.append("    organization: ").append(toIndentedString(organization)).append("\n");
			return sb.toString();
		}

		/**
		 * Convert the given object to string with each line indented by 4
		 * spaces (except the first line).
		 */
		private String toIndentedString(java.lang.Object o) {
			if (o == null) {
				return "null";
			}
			return o.toString().replace("\n", "\n    ");
		}
	}

	/**
	 *
	 * UserInfo model with the following properties:
	 * <br><b>sub</b>: the user ID GUID.
	 * <br><b>accounts</b>: this is list of DocuSign accounts associated with the current user.
	 * <br><b>name</b>: the user's full name.
	 * <br><b>givenName</b>: the user's given name.
	 * <br><b>familyName</b>: the user's family name.
	 * <br><b>email</b>: the user's email address.
	 * <br><b>created</b>: the UTC DateTime when the user login was created.
	 *
	 * @see Account
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class UserInfo {

		@JsonProperty("sub")
		private String sub = null;

		@JsonProperty("email")
		private String email = null;

		@JsonProperty("accounts")
		private java.util.List<Account> accounts = new java.util.ArrayList<Account>();

		@JsonProperty("name")
		private String name = null;

		@JsonProperty("given_name")
		private String givenName = null;

		@JsonProperty("family_name")
		private String familyName = null;

		@JsonProperty("created")
		private String created = null;

		public UserInfo sub(String sub) {
			this.sub = sub;
			return this;
		}

		/**
		 * Get sub
		 *
		 * @return sub
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getSub() {
			return sub;
		}

		public void setSub(String sub) {
			this.sub = sub;
		}

		public UserInfo email(String email) {
			this.email = email;
			return this;
		}

		/**
		 * Get email
		 *
		 * @return email
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getEmail() {
			return email;
		}

		public void setEmail(String email) {
			this.email = email;
		}

		public UserInfo accounts(java.util.List<Account> accounts) {
			this.accounts = accounts;
			return this;
		}

		public UserInfo addAccountsItem(Account accountsItem) {
			this.accounts.add(accountsItem);
			return this;
		}

		/**
		 * Get accounts
		 *
		 * @return accounts
		 **/
		@ApiModelProperty(example = "null", value = "")
		public java.util.List<Account> getAccounts() {
			return accounts;
		}

		public void setAccounts(java.util.List<Account> accounts) {
			this.accounts = accounts;
		}

		public UserInfo name(String name) {
			this.name = name;
			return this;
		}

		/**
		 * Get name
		 *
		 * @return name
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public UserInfo givenName(String givenName) {
			this.givenName = givenName;
			return this;
		}

		/**
		 * Get givenName
		 *
		 * @return givenName
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getGivenName() {
			return givenName;
		}

		public void setGivenName(String givenName) {
			this.givenName = givenName;
		}

		public UserInfo familyName(String familyName) {
			this.familyName = familyName;
			return this;
		}

		/**
		 * Get familyName
		 *
		 * @return familyName
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getFamilyName() {
			return familyName;
		}

		public void setFamilyName(String familyName) {
			this.familyName = familyName;
		}

		public UserInfo created(String created) {
			this.created = created;
			return this;
		}

		/**
		 * Get created
		 *
		 * @return created
		 **/
		@ApiModelProperty(example = "null", value = "")
		public String getCreated() {
			return created;
		}

		public void setCreated(String created) {
			this.created = created;
		}

		@Override
		public boolean equals(java.lang.Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			UserInfo userInfo = (UserInfo) o;
			return Objects.equals(this.sub, userInfo.sub) && Objects.equals(this.email, userInfo.email)
					&& Objects.equals(this.accounts, userInfo.accounts) && Objects.equals(this.name, userInfo.name)
					&& Objects.equals(this.givenName, userInfo.givenName) && Objects.equals(this.familyName, userInfo.familyName)
					&& Objects.equals(this.created, userInfo.created);
		}

		@Override
		public int hashCode() {
			return Objects.hash(sub, email, accounts, name, givenName, familyName, created);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("class UserInfo {\n");

			sb.append("    sub: ").append(toIndentedString(sub)).append("\n");
			sb.append("    email: ").append(toIndentedString(email)).append("\n");
			sb.append("    name: ").append(toIndentedString(name)).append("\n");
			sb.append("    givenName: ").append(toIndentedString(givenName)).append("\n");
			sb.append("    familyName: ").append(toIndentedString(familyName)).append("\n");
			sb.append("    created: ").append(toIndentedString(created)).append("\n");
			sb.append("    accounts: ").append(toIndentedString(accounts)).append("\n");
			return sb.toString();
		}

		/**
		 * Convert the given object to string with each line indented by 4 spaces
		 * (except the first line).
		 */
		private String toIndentedString(java.lang.Object o) {
			if (o == null) {
				return "null";
			}
			return o.toString().replace("\n", "\n    ");
		}

	}
}