package com.hellosign.sdk;

import com.hellosign.sdk.http.Authentication;
import com.hellosign.sdk.http.HttpClient;
import com.hellosign.sdk.resource.AbstractRequest;
import com.hellosign.sdk.resource.AbstractResourceList;
import com.hellosign.sdk.resource.Account;
import com.hellosign.sdk.resource.ApiApp;
import com.hellosign.sdk.resource.BulkSendJobById;
import com.hellosign.sdk.resource.EmbeddedRequest;
import com.hellosign.sdk.resource.EmbeddedResponse;
import com.hellosign.sdk.resource.FileUrlResponse;
import com.hellosign.sdk.resource.SignatureRequest;
import com.hellosign.sdk.resource.Team;
import com.hellosign.sdk.resource.Template;
import com.hellosign.sdk.resource.TemplateDraft;
import com.hellosign.sdk.resource.TemplateSignatureRequest;
import com.hellosign.sdk.resource.UnclaimedDraft;
import com.hellosign.sdk.resource.support.ApiAppList;
import com.hellosign.sdk.resource.support.OauthData;
import com.hellosign.sdk.resource.support.Signature;
import com.hellosign.sdk.resource.support.SignatureRequestList;
import com.hellosign.sdk.resource.support.TemplateList;
import com.hellosign.sdk.resource.support.types.BulkSendJobsList;
import com.hellosign.sdk.resource.support.types.FieldType;
import java.io.File;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * You'll need the HelloSignClient to do just about everything, from creating signatures to updating
 * account information.
 *
 * To use this class, instantiate a client with valid credentials like so:
 *
 * HelloSignClient client = new HelloSignClient(user, key);
 *
 * Then, use the client to perform your requests. The client uses java.net.HttpURLConnection to
 * perform its HTTP requests.
 */
public class HelloSignClient {

    public static final String DEFAULT_ENCODING = "UTF-8";
    public static final String DEFAULT_BASE_API_URL = "https://api.hellosign.com/v3";
    public static final String DEFAULT_OAUTH_TOKEN_URL = "https://app.hellosign.com/oauth/token";

    public static final String ACCOUNT_URI = "/account";
    public static final String VALIDATE_ACCOUNT_URI = "/account/validate";
    public static final String ACCOUNT_CREATE_URI = "/account/create";
    public static final String TEAM_URI = "/team";
    public static final String TEAM_CREATE_URI = "/team/create";
    public static final String TEAM_DESTROY_URI = "/team/destroy";
    public static final String TEAM_ADD_MEMBER_URI = "/team/add_member";
    public static final String TEAM_REMOVE_MEMBER_URI = "/team/remove_member";
    public static final String SIGNATURE_REQUEST_URI = "/signature_request";
    public static final String SIGNATURE_REQUEST_LIST_URI = "/signature_request/list";
    public static final String SIGNATURE_REQUEST_SEND_URI = "/signature_request/send";
    public static final String TEMPLATE_URI = "/template";
    public static final String TEMPLATE_FILE_URI = "/template/files";
    public static final String TEMPLATE_LIST_URI = "/template/list";
    public static final String TEMPLATE_ADD_USER_URI = "/template/add_user";
    public static final String TEMPLATE_REMOVE_USER_URI = "/template/remove_user";
    public static final String TEMPLATE_DELETE_URI = "/template/delete";
    public static final String TEMPLATE_UPDATE_FILES_URI = "/template/update_files";
    public static final String TEMPLATE_CREATE_EMBEDDED_DRAFT_URI = "/template/create_embedded_draft";
    public static final String TEMPLATE_SIGNATURE_REQUEST_URI = "/signature_request/send_with_template";
    public static final String SIGNATURE_REQUEST_CANCEL_URI = "/signature_request/cancel";
    public static final String SIGNATURE_REQUEST_REMIND_URI = "/signature_request/remind";
    public static final String SIGNATURE_REQUEST_FINAL_COPY_URI = "/signature_request/final_copy";
    public static final String SIGNATURE_REQUEST_FILES_URI = "/signature_request/files";
    public static final String SIGNATURE_REQUEST_UPDATE_URI = "/signature_request/update";
    public static final String SIGNATURE_REQUEST_EMBEDDED_URI = "/signature_request/create_embedded";
    public static final String SIGNATURE_REQUEST_EMBEDDED_TEMPLATE_URI = "/signature_request/create_embedded_with_template";
    public static final String EMBEDDED_SIGN_URL_URI = "/embedded/sign_url";
    public static final String EMBEDDED_EDIT_URL_URI = "/embedded/edit_url";
    public static final String UNCLAIMED_DRAFT_CREATE_URI = "/unclaimed_draft/create";
    public static final String UNCLAIMED_DRAFT_CREATE_EMBEDDED_URI = "/unclaimed_draft/create_embedded";
    public static final String UNCLAIMED_DRAFT_CREATE_EMBEDDED_WITH_TEMPLATE_URI = "/unclaimed_draft/create_embedded_with_template";
    public static final String API_APP_URI = "/api_app";
    public static final String API_APP_LIST_URI = "/api_app/list";
    public static final String BULK_SEND_JOBS_LIST = "/bulk_send_job/list";
    public static final String BULK_SEND_JOBS = "/bulk_send_job/";
    public static final String SIGNATURE_REQUEST_REMOVE_URI = "/signature_request/remove";

    public static final String PARAM_FILE_TYPE_URI = "file_type";
    public static final String PARAM_GET_URL = "get_url";
    public static final String FINAL_COPY_FILE_NAME = "final-copy";
    public static final String FINAL_COPY_FILE_EXT = "pdf";
    public static final String FILES_FILE_NAME = "files";
    public static final String FILES_FILE_EXT = "pdf";
    public static final String TEMPLATE_FILE_NAME = "template";
    public static final String TEMPLATE_FILE_EXT = "pdf";
    public static final String OAUTH_CODE = "code";
    public static final String OAUTH_STATE = "state";
    public static final String OAUTH_GRANT_TYPE = "grant_type";
    public static final String OAUTH_REFRESH_TOKEN = "refresh_token";
    public static final String OAUTH_GRANT_TYPE_AUTHORIZE_CODE = "authorization_code";
    public static final String OAUTH_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
    public static final String CLIENT_SECRET = "client_secret";
    public static final String CLIENT_ID = "client_id";
    public static final String EMBEDDED_TEMPLATE_SKIP_SIGNER_ROLES = "skip_signer_roles";
    public static final String EMBEDDED_TEMPLATE_SKIP_SUBJECT_MESSAGE = "skip_subject_message";
    public static final String EMBEDDED_TEMPLATE_MERGE_FIELDS = "merge_fields";
    public static final String EMBEDDED_TEMPLATE_CC_ROLES = "cc_roles";

    private Authentication auth;
    private HttpClient httpClient;

    // The base URL for all standard API endpoints, which can be
    // overridden by setting the "hellosign.base.url" system property.
    private String BASE_URI;

    // The base URL for retrieving an OAuth tokens, which can be
    // overridden by setting the "hellosign.oauth.base.url" system property.
    private String OAUTH_TOKEN_URL;

    /**
     * Default constructor for injection of dependencies (testing).
     *
     * @param client HttpClient
     * @param auth Authentication
     * @see #HelloSignClient(String)
     */
    protected HelloSignClient(HttpClient client, Authentication auth) {
        this.httpClient = client;
        this.auth = auth;

        // Set overrides if present
        BASE_URI = DEFAULT_BASE_API_URL;
        String baseUrl = System.getProperty("hellosign.base.url");
        if (baseUrl != null && !baseUrl.isEmpty()) {
            BASE_URI = baseUrl;
        }
        OAUTH_TOKEN_URL = DEFAULT_OAUTH_TOKEN_URL;
        String customOauthToken = System.getProperty("hellosign.oauth.base.url");
        if (customOauthToken != null && !customOauthToken.isEmpty()) {
            OAUTH_TOKEN_URL = customOauthToken;
        }
    }

    /**
     * Creates a new HelloSign client using your API key.
     *
     * @param apiKey String API key
     * @see <a href= "https://app.hellosign.com/home/myAccount/current_tab/api">Account Settings</a>
     */
    public HelloSignClient(String apiKey) {
        this(new HttpClient(), new Authentication(apiKey));
        auth.setApiKey(apiKey);
    }

    /**
     * Creates a new HelloSign client using then given Authentication object.
     *
     * @param auth Authentication used primarily for setting OAuth token/secret
     */
    public HelloSignClient(Authentication auth) {
        this(new HttpClient(), auth);
    }

    /**
     * Allows overriding of the authentication mechanism. Used mainly for setting an OAuth
     * token/secret.
     *
     * @param auth Authentication used for setting the auth method for this client
     */
    public void setAuthentication(Authentication auth) {
        this.auth = auth;
    }

    /**
     * Retrieves the authentication details being used by this client. Used for testing purposes.
     *
     * @return Authentication
     */
    protected Authentication getAuth() {
        return auth;
    }

    /**
     * Returns the current user's account information.
     *
     * @return Account
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Account getAccount() throws HelloSignException {
        return new Account(httpClient.withAuth(auth).get(BASE_URI + ACCOUNT_URI).asJson());
    }

    /**
     * Returns true if an account exists with the provided email address. Note this is limited to
     * the visibility of the currently authenticated user.
     *
     * @param email String email address
     * @return true if the account exists, false otherwise
     * @throws HelloSignException Thrown if there's a problem communicating with the HelloSign API.
     */
    public boolean isAccountValid(String email) throws HelloSignException {
        if (email == null || email.isEmpty()) {
            return false;
        }
        Account account = new Account(
            httpClient.withAuth(auth).withPostField(Account.ACCOUNT_EMAIL_ADDRESS, email)
                .post(BASE_URI + VALIDATE_ACCOUNT_URI).asJson());
        return (account.hasEmail() && email.equalsIgnoreCase(account.getEmail()));
    }

    /**
     * Update your account callback URL.
     *
     * This URL is used to notify you of any signature request events that occur when your account
     * is a party -- e.g., sender or signer/recipient.
     *
     * @param callback String URL
     * @return Account
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Account setCallback(String callback) throws HelloSignException {
        return new Account(
            httpClient.withAuth(auth).withPostField(Account.ACCOUNT_CALLBACK_URL, callback)
                .post(BASE_URI + ACCOUNT_URI).asJson());
    }

    /**
     * Creates a new HelloSign account. The user will still need to validate their email address to
     * complete the creation process to set a password. Note: This request does not require
     * authentication, so just performs the basic POST.
     *
     * @param email String New user's email address
     * @return Account New user's account information
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Account createAccount(String email) throws HelloSignException {
        return createAccount(email, null, null);
    }

    /**
     * Creates a new HelloSign account and provides OAuth app credentials to automatically generate
     * an OAuth token with the user Account response.
     *
     * @param email String New user's email address
     * @param clientId String Client ID
     * @param clientSecret String App secret
     * @return Account New user's account information
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Account createAccount(String email, String clientId, String clientSecret)
        throws HelloSignException {
        HttpClient client = httpClient.withAuth(auth)
            .withPostField(Account.ACCOUNT_EMAIL_ADDRESS, email);
        if (clientId != null && clientSecret != null) {
            client = client.withPostField(CLIENT_ID, clientId)
                .withPostField(CLIENT_SECRET, clientSecret);
        }
        JSONObject response = client.post(BASE_URI + ACCOUNT_CREATE_URI).asJson();
        JSONObject copy;
        try {
            copy = new JSONObject(response.toString());
        } catch (JSONException e) {
            throw new HelloSignException(e);
        }
        OauthData data = new OauthData(copy);
        Account account = new Account(response);
        account.setOauthData(data);
        return account;
    }

    /**
     * Performs an OAuth request and returns the necessary data for authorizing an API application,
     * and will automatically set the access token and code for making authenticated requests with
     * this client.
     *
     * @param code String OAuth code
     * @param clientId String OAuth client ID
     * @param secret String OAuth secret
     * @param state String OAuth client state
     * @param autoSetRequestToken true if the token should be applied to this client for future
     * requests
     * @return OauthData object containing OAuth token details
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public OauthData getOauthData(String code, String clientId, String secret, String state,
        boolean autoSetRequestToken) throws HelloSignException {
        OauthData data = new OauthData(httpClient.withAuth(auth).withPostField(OAUTH_STATE, state)
            .withPostField(OAUTH_CODE, code).withPostField(CLIENT_ID, clientId)
            .withPostField(OAUTH_GRANT_TYPE, OAUTH_GRANT_TYPE_AUTHORIZE_CODE)
            .withPostField(CLIENT_SECRET, secret)
            .post(OAUTH_TOKEN_URL).asJson());
        if (autoSetRequestToken) {
            setAccessToken(data.getAccessToken(), data.getTokenType());
        }
        return data;
    }

    /**
     * Refreshes the OauthData for this client with the provided refresh token.
     *
     * @param refreshToken String
     * @return OauthData new OAuthData returned from HelloSign
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public OauthData refreshOauthData(String refreshToken) throws HelloSignException {
        OauthData data = new OauthData(
            httpClient.withAuth(auth)
                .withPostField(OAUTH_GRANT_TYPE, OAUTH_GRANT_TYPE_REFRESH_TOKEN)
                .withPostField(OAUTH_REFRESH_TOKEN, refreshToken).post(OAUTH_TOKEN_URL).asJson());
        setAccessToken(data.getAccessToken(), data.getTokenType());
        return data;
    }

    /**
     * Retrieves the Team for the current user account.
     *
     * @return Team
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Team getTeam() throws HelloSignException {
        return new Team(httpClient.withAuth(auth).get(BASE_URI + TEAM_URI).asJson());
    }

    /**
     * Creates a new team for the current user with the given name.
     *
     * @param teamName String team name
     * @return Team
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Team createTeam(String teamName) throws HelloSignException {
        return new Team(
            httpClient.withAuth(auth).withPostField(Team.TEAM_NAME, teamName)
                .post(BASE_URI + TEAM_URI).asJson());
    }

    /**
     * Destroys the current user's team.
     *
     * @return boolean if destroy was successful
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public boolean destroyTeam() throws HelloSignException {
        return HttpURLConnection.HTTP_OK == httpClient.withAuth(auth)
            .post(BASE_URI + TEAM_DESTROY_URI).asHttpCode();
    }

    /**
     * Updates the current user's team name.
     *
     * @param teamName String team name
     * @return Team
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Team updateTeamName(String teamName) throws HelloSignException {
        return new Team(
            httpClient.withAuth(auth).withPostField(Team.TEAM_NAME, teamName)
                .post(BASE_URI + TEAM_URI).asJson());
    }

    /**
     * Adds the user to the current user's team.
     *
     * @param idOrEmail String new team member's account ID or email address
     * @return Team
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Team inviteTeamMember(String idOrEmail) throws HelloSignException {
        String key = (idOrEmail != null && idOrEmail.contains("@")) ? Account.ACCOUNT_EMAIL_ADDRESS
            : Account.ACCOUNT_ID;
        return new Team(
            httpClient.withAuth(auth).withPostField(key, idOrEmail)
                .post(BASE_URI + TEAM_ADD_MEMBER_URI).asJson());
    }

    /**
     * Removes the team member indicated by the user account ID or email address.
     *
     * @param idOrEmail String removed team member's account ID or email address
     * @return Team
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Team removeTeamMember(String idOrEmail) throws HelloSignException {
        String key = (idOrEmail != null && idOrEmail.contains("@")) ? Account.ACCOUNT_EMAIL_ADDRESS
            : Account.ACCOUNT_ID;
        return new Team(httpClient.withAuth(auth).withPostField(key, idOrEmail)
            .post(BASE_URI + TEAM_REMOVE_MEMBER_URI)
            .asJson());
    }

    /**
     * Retrieves a Signature Request with the given ID.
     *
     * @param id String signature ID
     * @return SignatureRequest
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequest getSignatureRequest(String id) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_URI + "/" + id;
        return new SignatureRequest(httpClient.withAuth(auth).get(url).asJson());
    }

    /**
     * Retrieves the current user's signature requests. The resulting object represents a paged
     * query result. The page information can be retrieved on from the ListInfo object on the
     * SignatureRequestList.
     *
     * @return SignatureRequestList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequestList getSignatureRequests() throws HelloSignException {
        return new SignatureRequestList(
            httpClient.withAuth(auth).get(BASE_URI + SIGNATURE_REQUEST_LIST_URI).asJson());
    }

    /**
     * Retrieves a specific page of the current user's signature requests.
     *
     * @param page int
     * @return SignatureRequestList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequestList getSignatureRequests(int page) throws HelloSignException {
        return new SignatureRequestList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .get(BASE_URI + SIGNATURE_REQUEST_LIST_URI).asJson());
    }

    /**
     * Retrieves a specific page of the current user's signature requests.
     *
     * @param page int
     * @param pageSize int Must be between 1 and 100.
     * @return SignatureRequestList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequestList getSignatureRequests(int page, int pageSize)
        throws HelloSignException {
        return new SignatureRequestList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .withGetParam(AbstractResourceList.PAGE_SIZE, Integer.toString(pageSize))
                .get(BASE_URI + SIGNATURE_REQUEST_LIST_URI).asJson());
    }

    /**
     * Sends the provided signature request to HelloSign.
     *
     * @param req SignatureRequest
     * @return SignatureRequest
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequest sendSignatureRequest(SignatureRequest req) throws HelloSignException {
        if (req.hasId()) {
            throw new HelloSignException("Sending an existing signature request is not supported");
        }
        return new SignatureRequest(httpClient.withAuth(auth).withPostFields(req.getPostFields())
            .post(BASE_URI + SIGNATURE_REQUEST_SEND_URI).asJson());
    }

    /**
     * Update a signer's email address.
     *
     * This method requires the ID of the siganture request that has already been sent, as well as
     * the signature_id that represents the signer that should be updated. The ema
     *
     * @param signatureRequestId String ID of the signature request that has already been sent and
     * needs to be updated.
     * @param signatureId String ID of the signer that needs to be updated.
     * @param newEmailAddress String email address that the signer should be changed to
     * @return SignatureRequest The updated request data
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequest updateSignatureRequest(String signatureRequestId, String signatureId,
        String newEmailAddress) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_UPDATE_URI + "/" + signatureRequestId;
        return new SignatureRequest(
            httpClient.withAuth(auth).withPostField(Signature.SIGNATURE_ID, signatureId)
                .withPostField(SignatureRequest.SIGREQ_SIGNER_EMAIL, newEmailAddress).post(url)
                .asJson());
    }

    /**
     * Retrieves the templates for the current user account.
     *
     * @return TemplateList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public TemplateList getTemplates() throws HelloSignException {
        return new TemplateList(
            httpClient.withAuth(auth).get(BASE_URI + TEMPLATE_LIST_URI).asJson());
    }

    /**
     * Retrieves a page of templates for the current user account.
     *
     * @param page int
     * @return TemplateList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public TemplateList getTemplates(int page) throws HelloSignException {
        return new TemplateList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .get(BASE_URI + TEMPLATE_LIST_URI).asJson());
    }

    /**
     * Retrieves a page of templates with a specific pageSize
     *
     * @param page int
     * @param pageSize int Must be between 1 and 100.
     * @return TemplateList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public TemplateList getTemplates(int page, int pageSize) throws HelloSignException {
        return new TemplateList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .withGetParam(AbstractResourceList.PAGE_SIZE, Integer.toString(pageSize))
                .get(BASE_URI + TEMPLATE_LIST_URI).asJson());
    }

    /**
     * Retrieves the PDF file backing the Template specified by the provided Template ID.
     *
     * @param templateId String Template ID
     * @return File PDF file object
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public File getTemplateFile(String templateId) throws HelloSignException {
        String url = BASE_URI + TEMPLATE_FILE_URI + "/" + templateId;
        String fileName = TEMPLATE_FILE_NAME + "." + TEMPLATE_FILE_EXT;
        return httpClient.withAuth(auth).get(url).asFile(fileName);
    }

    /**
     * Returns a signed URL that can be used to retrieve the file backing a template.
     *
     * @param templateId String Template ID
     * @return String URL or null if no file URL can be retrieved
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public String getTemplateFileUrl(String templateId) throws HelloSignException {
        String fileUrl = null;
        String url = BASE_URI + TEMPLATE_FILE_URI + "/" + templateId;
        JSONObject response = httpClient.withAuth(auth).withGetParam(PARAM_GET_URL, "1").get(url)
            .asJson();
        if (response.has("file_url")) {
            try {
                fileUrl = response.getString("file_url");
            } catch (JSONException ex) {
                throw new HelloSignException(ex);
            }
        }
        return fileUrl;
    }

    /**
     * Retrieves a specific Template based on the provided Template ID.
     *
     * @param templateId String Template ID
     * @return Template
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Template getTemplate(String templateId) throws HelloSignException {
        String url = BASE_URI + TEMPLATE_URI + "/" + templateId;
        return new Template(httpClient.withAuth(auth).get(url).asJson());
    }

    /**
     * Adds the provided user to the template indicated by the provided template ID. The new user
     * can be designated using their account ID or email address.
     *
     * @param templateId String template ID
     * @param idOrEmail String account ID or email address
     * @return Template
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Template addTemplateUser(String templateId, String idOrEmail) throws HelloSignException {
        String url = BASE_URI + TEMPLATE_ADD_USER_URI + "/" + templateId;
        String key = (idOrEmail != null && idOrEmail.contains("@")) ? Account.ACCOUNT_EMAIL_ADDRESS
            : Account.ACCOUNT_ID;
        return new Template(
            httpClient.withAuth(auth).withPostField(key, idOrEmail).post(url).asJson());
    }

    /**
     * Delete the template designated by the template id
     *
     * @param templateId String template ID
     * @return true if the delete was successful, false otherwise
     * @throws HelloSignException thrown if there is a problem processing the HTTP request
     */
    public boolean deleteTemplate(String templateId) throws HelloSignException {
        String url = BASE_URI + TEMPLATE_DELETE_URI + "/" + templateId;
        return HttpURLConnection.HTTP_OK == httpClient.withAuth(auth).post(url).asHttpCode();
    }

    /**
     * Replaces the backing documents for the given template with the given files.
     *
     * Note that certain rules apply to this endpoint: https://app.hellosign.com/api/reference#update_template_files
     *
     * @param existingTemplateId String ID of the template from which the overlay data (signatures,
     * text fields, etc.) will be retrieved.
     * @param newTemplate TemplateDraft that holds the data and documents which will be used as the
     * basis for the new template. The following fields can be set in this request: files or
     * file_urls, subject, message, and test_mode.
     * @param clientId String optional ID of the app which is generating this new template. Set to
     * null if not used.
     * @return String ID of the template to be created
     * @throws HelloSignException thrown if there is a problem processing the HTTP request
     */
    public String updateTemplateFiles(String existingTemplateId, TemplateDraft newTemplate,
        String clientId)
        throws HelloSignException {
        String url = BASE_URI + TEMPLATE_UPDATE_FILES_URI + "/" + existingTemplateId;
        HttpClient client = httpClient.withAuth(auth).withPostFields(newTemplate.getPostFields());
        if (clientId != null) {
            client = client.withPostField(EmbeddedRequest.EMBEDDED_CLIENT_ID, clientId);
        }
        Template t = new Template(client.post(url).asJson());
        return t.getId();
    }

    /**
     * Adds the provided user to the template indicated by the provided template ID. The new user
     * can be designated using their account ID or email address.
     *
     * @param templateId String template ID
     * @param idOrEmail String account ID or email address
     * @return Template
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public Template removeTemplateUser(String templateId, String idOrEmail)
        throws HelloSignException {
        String url = BASE_URI + TEMPLATE_REMOVE_USER_URI + "/" + templateId;
        String key = (idOrEmail != null && idOrEmail.contains("@")) ? Account.ACCOUNT_EMAIL_ADDRESS
            : Account.ACCOUNT_ID;
        return new Template(
            httpClient.withAuth(auth).withPostField(key, idOrEmail).post(url).asJson());
    }

    /**
     * Creates a new Signature Request based on the template provided.
     *
     * @param req TemplateSignatureRequest
     * @return SignatureRequest
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequest sendTemplateSignatureRequest(TemplateSignatureRequest req)
        throws HelloSignException {
        return new SignatureRequest(httpClient.withAuth(auth).withPostFields(req.getPostFields())
            .post(BASE_URI + TEMPLATE_SIGNATURE_REQUEST_URI).asJson());
    }

    /**
     * Cancels an existing signature request. If it has been completed, it will delete the signature
     * request from your account.
     *
     * @param id SignatureRequest id
     * @return boolean true if successful
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public boolean cancelSignatureRequest(String id) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_CANCEL_URI + "/" + id;
        return HttpURLConnection.HTTP_OK == httpClient.withAuth(auth).post(url).asHttpCode();
    }

    /**
     * Instructs HelloSign to email the given address with a reminder to sign the SignatureRequest
     * referenced by the given request ID.
     *
     * Note: You cannot send a reminder within 1 hours of the last reminder that was sent, manually
     * or automatically.
     *
     * @param requestId String SignatureRequest ID
     * @param email String email
     * @return SignatureRequest The request to be reminded
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public SignatureRequest requestEmailReminder(String requestId, String email)
        throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_REMIND_URI + "/" + requestId;
        return new SignatureRequest(
            httpClient.withAuth(auth).withPostField(Account.ACCOUNT_EMAIL_ADDRESS, email).post(url)
                .asJson());
    }

    /**
     * Retrieves the final PDF copy of a signature request, if it exists.
     *
     * @param requestId String SignatureRequest ID
     * @return File final copy file, or null if it does not yet exist
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     * @deprecated Use {{@link #getFiles(String)}
     */
    @Deprecated
    public File getFinalCopy(String requestId) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_FINAL_COPY_URI + "/" + requestId;
        String filename = FINAL_COPY_FILE_NAME + "." + FINAL_COPY_FILE_EXT;
        return httpClient.withAuth(auth).get(url).asFile(filename);
    }

    /**
     * Retrieves a PDF copy of the files associated with a signature request.
     *
     * @param requestId String signature ID
     * @return File PDF file
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public File getFiles(String requestId) throws HelloSignException {
        return getFiles(requestId, SignatureRequest.SIGREQ_FORMAT_PDF);
    }

    /**
     * Retrieves the file associated with a signature request.
     *
     * @param requestId String signature request ID
     * @param format String format, see SignatureRequest for available types
     * @return File
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public File getFiles(String requestId, String format) throws HelloSignException {
        if (format == null || format.isEmpty()) {
            format = FILES_FILE_EXT;
        }
        String url = BASE_URI + SIGNATURE_REQUEST_FILES_URI + "/" + requestId;
        String fileName = FILES_FILE_NAME + "." + format;
        return httpClient.withAuth(auth).withGetParam(PARAM_FILE_TYPE_URI, format).get(url)
            .asFile(fileName);
    }

    /**
     * Retrieves a URL for a file associated with a signature request.
     *
     * @param requestId String signature request ID
     * @return {@link FileUrlResponse}
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     * @see <a href="https://app.hellosign.com/api/reference#get_files">https://app.hellosign.com/api/reference#get_files</a>
     */
    public FileUrlResponse getFilesUrl(String requestId) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_FILES_URI + "/" + requestId;

        HttpClient client = this.httpClient.withAuth(auth).withGetParam(PARAM_GET_URL, "1")
            .get(url);

        if (client.getLastResponseCode() == 404) {
            throw new HelloSignException(
                String.format("Could not find request with id=%s", requestId));
        }

        return new FileUrlResponse(client.asJson());
    }

    /**
     * Creates a signature request that can be embedded within your website.
     *
     * @param embeddedReq EmbeddedRequest
     * @return SignatureRequest
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public AbstractRequest createEmbeddedRequest(EmbeddedRequest embeddedReq)
        throws HelloSignException {
        String url = BASE_URI;
        Class<?> returnType = SignatureRequest.class;
        AbstractRequest req = embeddedReq.getRequest();
        if (req instanceof TemplateSignatureRequest) {
            url += SIGNATURE_REQUEST_EMBEDDED_TEMPLATE_URI;
        } else if (req instanceof UnclaimedDraft) {
            if (((UnclaimedDraft) req).getRequest() instanceof TemplateSignatureRequest) {
                url += UNCLAIMED_DRAFT_CREATE_EMBEDDED_WITH_TEMPLATE_URI;
            } else {
                url += UNCLAIMED_DRAFT_CREATE_EMBEDDED_URI;
            }
            returnType = UnclaimedDraft.class;
        } else {
            url += SIGNATURE_REQUEST_EMBEDDED_URI;
        }
        try {
            Constructor<?> constructor = returnType.getConstructor(JSONObject.class);
            return (AbstractRequest) constructor.newInstance(
                httpClient.withAuth(auth).withPostFields(embeddedReq.getPostFields()).post(url)
                    .asJson());
        } catch (Exception ex) {
            throw new HelloSignException(ex);
        }
    }

    /**
     * Retrieves the necessary information to build an embedded signature request.
     *
     * @param signatureId String ID of the signature request to embed
     * @return EmbeddedResponse
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public EmbeddedResponse getEmbeddedSignUrl(String signatureId) throws HelloSignException {
        String url = BASE_URI + EMBEDDED_SIGN_URL_URI + "/" + signatureId;
        return new EmbeddedResponse(httpClient.withAuth(auth).post(url).asJson());
    }

    /**
     * Retrieves the necessary information to edit an embedded template.
     *
     * @param templateId String ID of the signature request to embed
     * @return EmbeddedResponse
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public EmbeddedResponse getEmbeddedTemplateEditUrl(String templateId)
        throws HelloSignException {
        return getEmbeddedTemplateEditUrl(templateId, false, false, false);
    }

    /**
     * Retrieves the necessary information to edit an embedded template.
     *
     * @param templateId String ID of the signature request to embed
     * @param skipSignerRoles true if the edited template should not allow the user to modify the
     * template's signer roles. Defaults to false.
     * @param skipSubjectMessage true if the edited template should not allow the user to modify the
     * template's subject and message. Defaults to false.
     * @return EmbeddedResponse
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public EmbeddedResponse getEmbeddedTemplateEditUrl(String templateId, boolean skipSignerRoles,
        boolean skipSubjectMessage) throws HelloSignException {
        return getEmbeddedTemplateEditUrl(templateId, skipSignerRoles, skipSubjectMessage, false);
    }

    /**
     * Retrieves the necessary information to edit an embedded template.
     *
     * @param templateId String ID of the signature request to embed
     * @param skipSignerRoles true if the edited template should not allow the user to modify the
     * template's signer roles. Defaults to false.
     * @param skipSubjectMessage true if the edited template should not allow the user to modify the
     * template's subject and message. Defaults to false.
     * @param testMode true if this request is a test request. Useful for editing locked templates.
     * @return EmbeddedResponse
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public EmbeddedResponse getEmbeddedTemplateEditUrl(String templateId, boolean skipSignerRoles,
        boolean skipSubjectMessage, boolean testMode) throws HelloSignException {
        return getEmbeddedTemplateEditUrl(templateId, skipSignerRoles, skipSubjectMessage, false,
            null, null);
    }

    /**
     * Big kahuna method for editing an embedded template. This method allows the updating of merge
     * fields and CC roles (both optional parameters).
     *
     * @param templateId String ID of the signature request to embed
     * @param skipSignerRoles true if the edited template should not allow the user to modify the
     * template's signer roles. Defaults to false.
     * @param skipSubjectMessage true if the edited template should not allow the user to modify the
     * template's subject and message. Defaults to false.
     * @param testMode true if this request is a test request. Useful for editing locked templates.
     * @param mergeFields These will overwrite the current merge fields for the embedded template.
     * To remove all merge fields, pass in an empty Map.
     * @param ccRoles These will overwrite the current CC Roles for the embedded template. To remove
     * all CC roles, pass in null or an empty List.
     * @return EmbeddedResponse
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public EmbeddedResponse getEmbeddedTemplateEditUrl(String templateId, boolean skipSignerRoles,
        boolean skipSubjectMessage, boolean testMode, Map<String, FieldType> mergeFields,
        List<String> ccRoles)
        throws HelloSignException {
        String url = BASE_URI + EMBEDDED_EDIT_URL_URI + "/" + templateId;
        HttpClient client = httpClient.withAuth(auth)
            .withPostField(EMBEDDED_TEMPLATE_SKIP_SIGNER_ROLES, skipSignerRoles)
            .withPostField(EMBEDDED_TEMPLATE_SKIP_SUBJECT_MESSAGE, skipSubjectMessage)
            .withPostField(AbstractRequest.REQUEST_TEST_MODE, testMode);
        String mergeFieldsStr = TemplateDraft.serializeMergeFields(mergeFields);
        if (mergeFieldsStr != null) {
            client = client.withPostField(EMBEDDED_TEMPLATE_MERGE_FIELDS, mergeFieldsStr);
        }
        if (ccRoles == null || ccRoles.isEmpty()) {
            // Per documentation: https://app.hellosign.com/api/reference#get_embedded_template_edit_url
            client = client.withPostField(EMBEDDED_TEMPLATE_CC_ROLES + "[0]", "");
        } else {
            for (int i = 0; i < ccRoles.size(); i++) {
                String cc = ccRoles.get(i);
                client = client.withPostField(EMBEDDED_TEMPLATE_CC_ROLES + "[" + i + "]", cc);
            }
        }
        return new EmbeddedResponse(client.post(url).asJson());
    }

    /**
     * Creates an unclaimed draft using the provided request draft object.
     *
     * @param draft UnclaimedDraft
     * @return UnclaimedDraft The created draft
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public UnclaimedDraft createUnclaimedDraft(UnclaimedDraft draft) throws HelloSignException {
        String url = BASE_URI;
        if (draft.isForEmbeddedSigning()) {
            url += UNCLAIMED_DRAFT_CREATE_EMBEDDED_URI;
        } else {
            url += UNCLAIMED_DRAFT_CREATE_URI;
        }
        return new UnclaimedDraft(
            httpClient.withAuth(auth).withPostFields(draft.getPostFields()).post(url).asJson());
    }

    /**
     * Creates a template draft that can be used for embedded template creation.
     *
     * @param req EmbeddedRequest
     * @return Template the unclaimed template draft
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public TemplateDraft createEmbeddedTemplateDraft(EmbeddedRequest req)
        throws HelloSignException {
        return new TemplateDraft(httpClient.withAuth(auth).withPostFields(req.getPostFields())
            .post(BASE_URI + TEMPLATE_CREATE_EMBEDDED_DRAFT_URI).asJson());
    }

    /**
     * Retrieves the API app configuration for the given Client ID.
     *
     * @param clientId String
     * @return ApiApp
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public ApiApp getApiApp(String clientId) throws HelloSignException {
        String url = BASE_URI + API_APP_URI + "/" + clientId;
        return new ApiApp(httpClient.withAuth(auth).get(url).asJson());
    }

    /**
     * Retrieves a paged list of API apps for the authenticated account.
     *
     * @return ApiAppList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public ApiAppList getApiApps() throws HelloSignException {
        return new ApiAppList(httpClient.withAuth(auth).get(BASE_URI + API_APP_LIST_URI).asJson());
    }

    /**
     * Creates a new ApiApp using the properties set on the provided ApiApp.
     *
     * @param app ApiApp
     * @return ApiApp newly created ApiApp
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public ApiApp createApiApp(ApiApp app) throws HelloSignException {
        return new ApiApp(
            httpClient.withAuth(auth).withPostFields(app.getPostFields())
                .post(BASE_URI + API_APP_URI).asJson());
    }

    /**
     * Attempts to delete the API app with the given client ID.
     *
     * @param clientId String The Client ID of the app that should be deleted.
     * @return boolean true if the API app was successfully deleted
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public boolean deleteApiApp(String clientId) throws HelloSignException {
        String url = BASE_URI + API_APP_URI + "/" + clientId;
        return HttpURLConnection.HTTP_NO_CONTENT == httpClient.withAuth(auth).delete(url)
            .asHttpCode();
    }

    /**
     * Updates the API app with the given ApiApp object properties.
     *
     * @param app ApiApp
     * @return ApiApp updated ApiApp
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public ApiApp updateApiApp(ApiApp app) throws HelloSignException {
        if (!app.hasClientId()) {
            throw new HelloSignException(
                "Cannot update an ApiApp without a client ID. Create one first!");
        }
        String url = BASE_URI + API_APP_URI + "/" + app.getClientId();
        return new ApiApp(
            httpClient.withAuth(auth).withPostFields(app.getPostFields()).post(url).asJson());
    }

    /**
     * Sets the access token for the OAuth user that this client will use to perform requests.
     *
     * @param accessToken String access token
     * @param tokenType String token type
     * @throws HelloSignException thrown if there's a problem setting the access token.
     */
    public void setAccessToken(String accessToken, String tokenType) throws HelloSignException {
        auth.setAccessToken(accessToken, tokenType);
    }

    /**
     * Returns a list of BulkSendJob that you can access.
     *
     * @return bulkSendJobsList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public BulkSendJobsList getBulkSendJobList() throws HelloSignException {
        return new BulkSendJobsList(httpClient.withAuth(auth).get(BASE_URI + BULK_SEND_JOBS_LIST).asJson());
    }

    /**
     * Retrieves a specific page number of the BulkSendJob List.
     *
     * @param page int
     * @return BulkSendJobsList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public BulkSendJobsList getBulkSendJobList(int page) throws HelloSignException {
        return new BulkSendJobsList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .get(BASE_URI + BULK_SEND_JOBS_LIST).asJson());
    }

    /**
     * Retrieves a specific page number of the BulkSendJob List, with Number of objects to be returned per page.
     * Must be between 1 and 100. Default is 20.
     *
     * @param page int
     * @param pageSize int Must be between 1 and 100.
     * @return BulkSendJobsList
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public BulkSendJobsList getBulkSendJobList(int page, int pageSize) throws HelloSignException {
        return new BulkSendJobsList(
            httpClient.withAuth(auth)
                .withGetParam(AbstractResourceList.PAGE, Integer.toString(page))
                .withGetParam(AbstractResourceList.PAGE_SIZE, Integer.toString(pageSize))
                .get(BASE_URI + BULK_SEND_JOBS_LIST).asJson());
    }

    /**
     * Returns the status of the BulkSendJob and its SignatureRequests specified by the bulk_send_job_id parameter.
     * @param bulk_send_job_id
     * @return
     * @throws HelloSignException
     */
    public BulkSendJobById getBulkSendByJobId(String bulk_send_job_id) throws HelloSignException{
        String url =  BASE_URI + BULK_SEND_JOBS + "/" + bulk_send_job_id;
        return new BulkSendJobById(httpClient.withAuth(auth).get(url).asJson());
    }

    /**
     * Removes your access to a completed signature request. This action is not reversible.
     *
     * The signature request must be fully executed by all parties (signed or declined to sign).
     * Other parties will continue to maintain access to the completed signature request document(s).
     *
     * @param signature_request_id SignatureRequest id
     * @return boolean true if successful
     * @throws HelloSignException thrown if there's a problem processing the HTTP request or the
     * JSON response.
     */
    public boolean removeSignatureRequestAccess(String signature_request_id) throws HelloSignException {
        String url = BASE_URI + SIGNATURE_REQUEST_REMOVE_URI + "/" + signature_request_id;
        return HttpURLConnection.HTTP_OK == httpClient.withAuth(auth).post(url).asHttpCode();
    }

}