public final class Account implements Serializable {

    private final Datacenter datacenter;

    private final String accountCode;

    /**
     * Construct an account instance.
     * @param datacenter the datacenter that hosts the account.
     * @param accountCode the customer account code.
     */
    public Account(Datacenter datacenter, String accountCode) {
        if (datacenter == null || StringUtils.isBlank(datacenter.getHost()) || StringUtils.isBlank(datacenter.getName())) {
            throw new IllegalArgumentException("A datacenter name and host must be provided");
        }
        if (StringUtils.isBlank(accountCode)) {
            throw new IllegalArgumentException("An account code must be provided");
        }
        this.datacenter = datacenter;
        this.accountCode = accountCode;
    }

    /**
     * Returns the datacenter
     * @return the datacenter that hosts the account.
     */
    public Datacenter getDatacenter() {
        return datacenter;
    }

    public String getAccountCode() {
        return accountCode;
    }

    @Override
    public String toString() {
        return "Account{" +
                "datacenter=" + datacenter +
                ", accountCode='" + accountCode + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        if (accountCode != null ? !accountCode.equals(account.accountCode) : account.accountCode != null) return false;
        if (datacenter != null ? !datacenter.equals(account.datacenter) : account.datacenter != null) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = datacenter != null ? datacenter.hashCode() : 0;
        result = 31 * result + (accountCode != null ? accountCode.hashCode() : 0);
        return result;
    }
} public final class UserCredentials implements Serializable {

    private final String emailAddress;

    private final String password;

    /**
     * Create an immutable credentials instance with a supplied email address and password.
     * @param emailAddress Email address of a user account within a Brightpearl customer account. Must be a non-empty string.
     * @param password The user's password. Must be a non-empty string.
     */
    public UserCredentials(String emailAddress, String password) {
        if (StringUtils.isBlank(emailAddress)) {
            throw new IllegalArgumentException("An email address must be supplied");
        }
        if (StringUtils.isBlank(password)) {
            throw new IllegalArgumentException("A password must be supplied");
        }
        this.emailAddress = emailAddress.trim();
        this.password = password.trim();
    }

    /**
     * Returns the email address.
     * @return the email address.
     */
    public String getEmailAddress() {
        return emailAddress;
    }

    /**
     * Returns the password.
     * @return the password.
     */
    public String getPassword() {
        return password;
    }

    @Override
    public String toString() {
        return "UserCredentials{" +
                "emailAddress='" + emailAddress + '\'' +
                ", password='" + (password == null ? "null" : "[redacted]") + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserCredentials that = (UserCredentials) o;
        if (emailAddress != null ? !emailAddress.equals(that.emailAddress) : that.emailAddress != null) return false;
        if (password != null ? !password.equals(that.password) : that.password != null) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = emailAddress != null ? emailAddress.hashCode() : 0;
        result = 31 * result + (password != null ? password.hashCode() : 0);
        return result;
    }
} public interface AppAuthorisation {

    static final String DEV_HEADER = "brightpearl-dev-ref";
    static final String APP_HEADER = "brightpearl-app-ref";
    static final String ACCOUNT_TOKEN_HEADER = "brightpearl-account-token";
    static final String STAFF_TOKEN_HEADER = "brightpearl-staff-token";

    static final String LEGACY_AUTH_HEADER = "brightpearl-auth";

    /**
     * Returns the Brightpearl customer's account.
     */
    Account getAccount();

    /**
     * Returns authentication headers required for requests made to the customer's account.
     */
    Map<String, String> getHeaders();

} public final class PrivateAppIdentity {

    private final String appReference;

    private final Account account;

    private PrivateAppIdentity(Account account, String appReference) {
        this.account = account;
        this.appReference = appReference;
    }

    /**
     * Create a private app identity.
     * @param account A Brightpearl customer account.
     * @param appReference The private app's unique reference.
     * @return An identity object.
     */
    public static PrivateAppIdentity create(Account account, String appReference) {
        if (account == null) { throw new IllegalArgumentException("Account must not be null"); }
        if (StringUtils.isBlank(appReference)) { throw new IllegalArgumentException("App reference must be supplied"); }
        return new PrivateAppIdentity(account, appReference);
    }

    public String getAppReference() {
        return appReference;
    }

    public Account getAccount() {
        return account;
    }

    @Override
    public String toString() {
        return "PrivateAppIdentity{" +
                "appReference='" + appReference + '\'' +
                ", account=" + account +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PrivateAppIdentity that = (PrivateAppIdentity) o;
        if (account != null ? !account.equals(that.account) : that.account != null) return false;
        if (appReference != null ? !appReference.equals(that.appReference) : that.appReference != null) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = appReference != null ? appReference.hashCode() : 0;
        result = 31 * result + (account != null ? account.hashCode() : 0);
        return result;
    }
} public final class PublicAppIdentity {

    private final String developerReference;

    private final String developerSecret;

    private final String appReference;

    private PublicAppIdentity(String developerReference, String developerSecret, String appReference) {
        this.developerReference = developerReference;
        this.developerSecret = developerSecret;
        this.appReference = appReference;
    }

    /**
     * Create a public app identity where the secret will not be required, either because all authentication will use
     * staff tokens or because account tokens will be supplied already signed.
     * @param developerReference Developer's unique reference.
     * @param appReference App's unique reference.
     * @return An identity object.
     */
    public static PublicAppIdentity create(String developerReference, String appReference) {
        if (StringUtils.isBlank(developerReference)) { throw new IllegalArgumentException("Developer reference must be supplied"); }
        if (StringUtils.isBlank(appReference)) { throw new IllegalArgumentException("App reference must be supplied"); }
        return new PublicAppIdentity(developerReference, null, appReference);
    }

    /**
     * Create a public app identity with an optional developer secret for signing account tokens.
     * @param developerReference Developer's unique reference.
     * @param appReference App's unique reference.
     * @return An identity object.
     */
    public static PublicAppIdentity create(String developerReference, String developerSecret, String appReference) {
        if (StringUtils.isBlank(developerReference)) { throw new IllegalArgumentException("Developer reference must be supplied"); }
        if (StringUtils.isBlank(appReference)) { throw new IllegalArgumentException("App reference must be supplied"); }
        return new PublicAppIdentity(developerReference, developerSecret, appReference);
    }

    public String getDeveloperReference() {
        return developerReference;
    }

    public String getDeveloperSecret() {
        return developerSecret;
    }

    public String getAppReference() {
        return appReference;
    }

    @Override
    public String toString() {
        return "PublicAppIdentity{" +
                "developerReference='" + developerReference + '\'' +
                ", developerSecret='" + (developerSecret == null ? "null" : "[redacted]") + '\'' +
                ", appReference='" + appReference + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PublicAppIdentity that = (PublicAppIdentity) o;
        if (appReference != null ? !appReference.equals(that.appReference) : that.appReference != null) return false;
        if (developerReference != null ? !developerReference.equals(that.developerReference) : that.developerReference != null)
            return false;
        if (developerSecret != null ? !developerSecret.equals(that.developerSecret) : that.developerSecret != null)
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = developerReference != null ? developerReference.hashCode() : 0;
        result = 31 * result + (developerSecret != null ? developerSecret.hashCode() : 0);
        result = 31 * result + (appReference != null ? appReference.hashCode() : 0);
        return result;
    }
} public class UserCredentialsWrapper {

    private final UserCredentials apiAccountCredentials;

    public UserCredentialsWrapper(UserCredentials apiAccountCredentials) {
        this.apiAccountCredentials = apiAccountCredentials;
    }

    public UserCredentials getApiAccountCredentials() {
        return apiAccountCredentials;
    }

} public class CalendarAdaptor extends TypeAdapter<Calendar> {

    private static DatatypeFactory datatypeFactory;
    private static Exception storedInitException;

    static {
        try {
            datatypeFactory = DatatypeFactory.newInstance();
        } catch (Exception e) {
            storedInitException = e;
        }
    }

    @Override
    public Calendar read(JsonReader jsonReader) throws IOException {
        if (jsonReader.peek() == JsonToken.NULL) {
            jsonReader.nextNull();
            return null;
        }
        String string = jsonReader.nextString();
        if (StringUtils.isNotBlank(string)) {
            if (storedInitException != null) {
                throw new JsonParseException("Could not parse '" + string + "' as an ISO date.", storedInitException);
            }
            try {
                return datatypeFactory.newXMLGregorianCalendar(string).toGregorianCalendar();
            } catch (Exception e) {
                throw new JsonParseException("Could not parse '" + string + "' as an ISO date.", e);
            }
        }
        return null;
    }

    @Override
    public void write(JsonWriter jsonWriter, Calendar calendar) throws IOException {
        if (calendar == null) {
            jsonWriter.nullValue();
            return;
        }
        if (storedInitException != null) {
            throw new JsonParseException("Unexpected error serializing calendar", storedInitException);
        }
        try {
            DatatypeFactory dtf = datatypeFactory;
            XMLGregorianCalendar xgc = dtf.newXMLGregorianCalendar();
            xgc.setYear(calendar.get(Calendar.YEAR));
            xgc.setDay(calendar.get(Calendar.DAY_OF_MONTH));
            xgc.setMonth(calendar.get(Calendar.MONTH) + 1);
            xgc.setHour(calendar.get(Calendar.HOUR_OF_DAY));
            xgc.setMinute(calendar.get(Calendar.MINUTE));
            xgc.setSecond(calendar.get(Calendar.SECOND));
            xgc.setMillisecond(calendar.get(Calendar.MILLISECOND));
            int offsetInMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
            xgc.setTimezone(offsetInMinutes);
            jsonWriter.value(xgc.toXMLFormat());
        } catch (Exception e) {
            throw new RuntimeException("Unexpected error serializing calendar", e);
        }
    }

} public class DateTimeAdaptor extends TypeAdapter<DateTime> {

    @Override
    public DateTime read(JsonReader jsonReader) throws IOException {
        if (jsonReader.peek() == JsonToken.NULL) {
            jsonReader.nextNull();
            return null;
        }
        String string = jsonReader.nextString();
        return ISODateTimeFormat.dateTimeParser().withOffsetParsed().parseDateTime(string);
    }

    @Override
    public void write(JsonWriter jsonWriter, DateTime dateTime) throws IOException {
        if (dateTime == null) {
            jsonWriter.nullValue();
            return;
        }
        jsonWriter.value(ISODateTimeFormat.dateTime().print(dateTime));
    }

} public class MultiMessage {

    private ProcessingMode processingMode;

    private OnFailOption onFail;

    private List<MultiMessageItem> messages;

    public MultiMessage(ProcessingMode processingMode, OnFailOption onFail, List<MultiMessageItem> messages) {
        this.processingMode = processingMode;
        this.onFail = onFail;
        this.messages = Collections.unmodifiableList(messages);
    }

    public ProcessingMode getProcessingMode() {
        return processingMode;
    }

    public OnFailOption getOnFail() {
        return onFail;
    }

    public List<MultiMessageItem> getMessages() {
        return messages;
    }
} public class MultiMessageItem {

    private String label;

    private String uri;

    private Method httpMethod;

    private Object body;

    public MultiMessageItem(String label, String uri, Method httpMethod, Object body) {
        this.label = label;
        this.uri = uri;
        this.httpMethod = httpMethod;
        this.body = body;
    }

    public String getLabel() {
        return label;
    }

    public String getUri() {
        return uri;
    }

    public Method getHttpMethod() {
        return httpMethod;
    }

    public Object getBody() {
        return body;
    }
} public final class MultiMessageResponse {

    private List<MultiMessageResponseItem> processedMessages;

    private List<String> unprocessedMessages;

    public List<MultiMessageResponseItem> getProcessedMessages() {
        return processedMessages;
    }

    public List<String> getUnprocessedMessages() {
        return unprocessedMessages;
    }
} public class MultiMessageResponseItem {

    private String label;

    private Integer statusCode;

    private MultiMessageResponseItemBody body;

    public String getLabel() {
        return label;
    }

    public Integer getStatusCode() {
        return statusCode;
    }

    public MultiMessageResponseItemBody getBody() {
        return body;
    }
} public class JsonWrapper {

    private JsonElement response;

    private List<ServiceError> errors;

    private Map<String, Map<String, Object>> reference;

    public List<ServiceError> getErrors() {
        return errors;
    }

    public JsonElement getResponse() {
        return response;
    }

    public Map<String, Map<String, Object>> getReference() {
        return reference;
    }

} public class PartialSearchResponse {

    private SearchResultsMetaData metaData;

    private List<JsonArray> results;

    public SearchResultsMetaData getMetaData() {
        return metaData;
    }

    public List<JsonArray> getResults() {
        return results;
    }
} public final class HttpClient4ClientFactoryBuilder {

    private int maxConnections = HttpClient4ClientFactory.DEFAULT_MAX_CONNECTIONS;
    private int maxConnectionsPerRoute = HttpClient4ClientFactory.DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
    private int connectionManagerTimeoutMs = HttpClient4ClientFactory.DEFAULT_CONNECTION_MANAGER_TIMEOUT_MS;
    private int connectionTimeoutMs = HttpClient4ClientFactory.DEFAULT_CONNECTION_TIMEOUT_MS;
    private int socketTimeoutMs = HttpClient4ClientFactory.DEFAULT_SOCKET_TIMEOUT_MS;
    private boolean allowRedirects = HttpClient4ClientFactory.DEFAULT_ALLOW_REDIRECTS;

    /**
     * Static builder method for method chaining, fluent builder style.
     * @return a new {@link HttpClient4ClientFactoryBuilder} instance.
     */
    public static HttpClient4ClientFactoryBuilder httpClient4ClientFactory() {
        return new HttpClient4ClientFactoryBuilder();
    }

    /**
     * Set the maximum total connections allowed across all routes. If creating a client for use with multiple Brightpearl
     * customer accounts there will be one route per datacenter.
     * @param maxConnections maximum total connections allowed for all routes.
     */
    public void setMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
    }

    /**
     * Set the maximum connections allowed per route. All Brightpearl customer accounts on a datacenter will share the
     * same route, so this is not a per-account limit.
     * @param maxConnectionsPerRoute maximum connections allowed per route.
     */
    public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) {
        this.maxConnectionsPerRoute = maxConnectionsPerRoute;
    }

    /**
     * Sets the connection manager timeout in milliseconds. This is the time a thread will wait for a connection to become
     * free when the connection pool is exhausted. Use zero for indefinite blocking.
     * @param connectionManagerTimeoutMs connection manager timeout in milliseconds.
     */
    public void setConnectionManagerTimeoutMs(int connectionManagerTimeoutMs) {
        this.connectionManagerTimeoutMs = connectionManagerTimeoutMs;
    }

    /**
     * Set the time in milliseconds to wait for a connection to be established before aborting the request. A value of
     * zero is interpreted as infinite timeout.
     * @param connectionTimeoutMs connection timeout in milliseconds.
     */
    public void setConnectionTimeoutMs(int connectionTimeoutMs) {
        this.connectionTimeoutMs = connectionTimeoutMs;
    }

    /**
     * Maximum time of inactivity to allow between two consecutive data packets from the server before aborting the
     * request. A value of zero is interpreted as an infinite timeout.
     * @param socketTimeoutMs socket timeout in milliseconds.
     */
    public void setSocketTimeoutMs(int socketTimeoutMs) {
        this.socketTimeoutMs = socketTimeoutMs;
    }

    /**
     * Set whether redirects should be followed. Redirects are not expected from the Brightpearl API according to the
     * documentation provided, and this setting is untested.
     * @param allowRedirects whether redirects should be followed.
     */
    public void setAllowRedirects(boolean allowRedirects) {
        this.allowRedirects = allowRedirects;
    }

    /**
     * Set the maximum total connections allowed across all routes. If creating a client for use with multiple Brightpearl
     * customer accounts there will be one route per datacenter.
     * @param maxConnections maximum total connections allowed for all routes.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
        return this;
    }

    /**
     * Set the maximum connections allowed per route. All Brightpearl customer accounts on a datacenter will share the
     * same route, so this is not a per-account limit.
     * @param maxConnectionsPerRoute maximum connections allowed per route.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withMaxConnectionsPerRoute(int maxConnectionsPerRoute) {
        this.maxConnectionsPerRoute = maxConnectionsPerRoute;
        return this;
    }

    /**
     * Sets the connection manager timeout in milliseconds. This is the time a thread will wait for a connection to become
     * free when the connection pool is exhausted.
     * @param connectionManagerTimeoutMs connection manager timeout in milliseconds.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withConnectionManagerTimeoutMs(int connectionManagerTimeoutMs) {
        this.connectionManagerTimeoutMs = connectionManagerTimeoutMs;
        return this;
    }

    /**
     * Set the time in milliseconds to wait for a connection to be established before aborting the request. A value of
     * zero is interpreted as infinite timeout.
     * @param connectionTimeoutMs connection timeout in milliseconds.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withConnectionTimeoutMs(int connectionTimeoutMs) {
        this.connectionTimeoutMs = connectionTimeoutMs;
        return this;
    }

    /**
     * Maximum time of inactivity to allow between two consecutive data packets from the server before aborting the
     * request. A value of zero is interpreted as an infinite timeout.
     * @param socketTimeoutMs socket timeout in milliseconds.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withSocketTimeoutMs(int socketTimeoutMs) {
        this.socketTimeoutMs = socketTimeoutMs;
        return this;
    }

    /**
     * Set whether redirects should be followed. Redirects are not expected from the Brightpearl API according to the
     * documentation provided, and this setting is untested.
     * @param allowRedirects whether redirects should be followed.
     * @return builder instance for method chaining.
     */
    public HttpClient4ClientFactoryBuilder withAllowRedirects(boolean allowRedirects) {
        this.allowRedirects = allowRedirects;
        return this;
    }

    /**
     * Builds an immutable {@link HttpClient4ClientFactory} instance with the configuration supplied.
     * @return a {@link HttpClient4ClientFactory} instance.
     */
    public HttpClient4ClientFactory build() {
        return new HttpClient4ClientFactory(
                maxConnections,
                maxConnectionsPerRoute,
                connectionManagerTimeoutMs,
                connectionTimeoutMs,
                socketTimeoutMs,
                allowRedirects);
    }

} public class NoOpRateLimiter implements RateLimiter {

    /**
     * This implementation does nothing.
     */
    @Override
    public void rateLimit(Account account) {
    }

    /**
     * This implementation does nothing.
     */
    @Override
    public void requestCompleted(Account account, int requestsRemaining, long nextThrottlePeriod) {
    }

    /**
     * This implementation does nothing.
     */
    @Override
    public void requestCapExceeded(Account account) {
    }
} public interface RateLimiter {

    /**
     * Called immediately before a request is made to the Brightpearl API for the given account. Implementations may
     * sleep before returning to reduce throughput, or throw an exception to abort the request.
     * @param account The account a request will be sent to.
     */
    void rateLimit(Account account);

    /**
     * Called after every request is made to the Brightpearl API, with values from the brightpearl-requests-remaining
     * and brightpearl-next-throttle-period headers. These may be used to adjust the behaviour of the {@link #rateLimit(Account)}
     * method, but as other requests may have been made on other threads while this request was executing, the values
     * may be stale, and events may be received out of sequence.
     * @param account The account a response has been received from.
     * @param requestsRemaining The value of the brightpearl-requests-remaining response header.
     * @param nextThrottlePeriod The value of the brightpearl-next-throttle-period header.
     */
    void requestCompleted(Account account, int requestsRemaining, long nextThrottlePeriod);

    /**
     * Called when a request to an account has been rejected due to request throttling. This event may be used to adjust
     * the behaviour of the the {@link #rateLimit(Account)} method. Other concurrent requests that were already started
     * at the time this event is received are likely to also fail.
     * @param account An account for which a request cap error has been received.
     */
    void requestCapExceeded(Account account);

} public class SearchColumn implements Serializable {

    private String name;
    private Boolean sortable;
    private Boolean filterable;
    private String reportDataType;
    private List<String> referenceData;
    private Boolean required;

    public String getName() {
        return name;
    }

    public Boolean getSortable() {
        return sortable;
    }

    public Boolean getFilterable() {
        return filterable;
    }

    public String getReportDataType() {
        return reportDataType;
    }

    public List<String> getReferenceData() {
        return referenceData;
    }

    public Boolean getRequired() {
        return required;
    }

} public class SearchResults<T> implements Serializable {

    private SearchResultsMetaData metaData;
    private List<T> results;
    private Map<String, Map<String, Object>> reference;

    public SearchResults(SearchResultsMetaData metaData, List<T> results, Map<String, Map<String, Object>> reference) {
        this.metaData = metaData;
        this.results = results;
        this.reference = reference;
    }

    public SearchResultsMetaData getMetaData() {
        return metaData;
    }

    public List<T> getResults() {
        return results;
    }

    public Map<String, Map<String, Object>> getReference() {
        return reference;
    }

} public final class StringUtils {

    private StringUtils() { }

    public static boolean isEmpty(String string) {
        return string == null || string.length() == 0;
    }

    public static boolean isNotEmpty(String string) {
        return !isEmpty(string);
    }

    public static boolean isBlank(String string) {
        return string == null || string.trim().length() == 0;
    }

    public static boolean isNotBlank(String string) {
        return !isBlank(string);
    }

    public static boolean equals(String lhs, String rhs) {
        if ((lhs == null) != (rhs == null)) {
            return false;
        } else if (lhs == null && rhs == null) {
            return true;
        } else {
            return lhs.equals(rhs);
        }
    }

    public static String join(Collection<String> strings, String separator) {
        StringBuilder builder = new StringBuilder();
        for (String string : strings) {
            if (builder.length() > 0) {
                builder.append(separator);
            }
            builder.append(string);
        }
        return builder.toString();
    }

}
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import uk.co.visalia.brightpearl.apiclient.account.Account;
import uk.co.visalia.brightpearl.apiclient.account.UserCredentials;
import uk.co.visalia.brightpearl.apiclient.auth.AppAuthorisation;
import uk.co.visalia.brightpearl.apiclient.auth.PrivateAppIdentity;
import uk.co.visalia.brightpearl.apiclient.auth.PublicAppIdentity;
import uk.co.visalia.brightpearl.apiclient.client.UserCredentialsWrapper;
import uk.co.visalia.brightpearl.apiclient.client.adaptors.CalendarAdaptor;
import uk.co.visalia.brightpearl.apiclient.client.adaptors.DateTimeAdaptor;
import uk.co.visalia.brightpearl.apiclient.client.multimessage.MultiMessage;
import uk.co.visalia.brightpearl.apiclient.client.multimessage.MultiMessageItem;
import uk.co.visalia.brightpearl.apiclient.client.multimessage.MultiMessageResponse;
import uk.co.visalia.brightpearl.apiclient.client.multimessage.MultiMessageResponseItem;
import uk.co.visalia.brightpearl.apiclient.client.parsing.JsonWrapper;
import uk.co.visalia.brightpearl.apiclient.client.parsing.PartialSearchResponse;
import uk.co.visalia.brightpearl.apiclient.exception.*;
import uk.co.visalia.brightpearl.apiclient.http.*;
import uk.co.visalia.brightpearl.apiclient.http.httpclient4.HttpClient4ClientFactoryBuilder;
import uk.co.visalia.brightpearl.apiclient.multimessage.*;
import uk.co.visalia.brightpearl.apiclient.ratelimit.NoOpRateLimiter;
import uk.co.visalia.brightpearl.apiclient.ratelimit.RateLimiter;
import uk.co.visalia.brightpearl.apiclient.request.*;
import uk.co.visalia.brightpearl.apiclient.search.SearchColumn;
import uk.co.visalia.brightpearl.apiclient.search.SearchResults;
import uk.co.visalia.brightpearl.apiclient.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.*;
/*
 * Copyright 2014 David Morrissey
 *
 * 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 uk.co.visalia.brightpearl.apiclient;



/**
 * <p>
 * Main client class for the Brightpearl API. This supports public and private apps of both types, system and staff. An
 * instance of this class is designed to be shared amongst many apps and accounts if necessary.
 * </p><p>
 * An instance is thread-safe assuming the instance of {@link uk.co.visalia.brightpearl.apiclient.http.ClientFactory} provided is
 * thread-safe, as all implementations provided with this library are. Instances are not specific to any account and can
 * be safely reused for multiple accounts.
 * </p>
 */
public final class BrightpearlApiClient {

    private static final String MULTIMESSAGE_TEMPLATE = "%s/public-api/%s/multi-message";
    private static final String MULTIMESSAGE_SERVICE_TEMPLATE = "/%s/%s";
    private static final String SERVICE_TEMPLATE = "%s/public-api/%s/%s/%s";
    private static final String AUTH_TEMPLATE = "%s/%s/authorise";

    private final ClientFactory clientFactory;
    private final RateLimiter rateLimiter;
    private final Gson gson;

    /**
     * Package private constructor for internal use only. {@link BrightpearlApiClientFactory} must be used to create new instances.
     */
    BrightpearlApiClient(ClientFactory clientFactory, RateLimiter rateLimiter, Gson gson) {
        this.clientFactory = clientFactory == null ? new HttpClient4ClientFactoryBuilder().build() : clientFactory;
        this.rateLimiter = rateLimiter == null ? new NoOpRateLimiter() : rateLimiter;
        this.gson = gson == null ? defaultGson() : gson;
    }

    /**
     * Using Brightpearl account details and user credentials, attempts authentication of the user and returns an auth
     * token if the request was successful. If the credentials are invalid, an {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlServiceException} will
     * be thrown, with an error code that, in most cases, starts with "GWY".
     * @param account Details of customer account to authenticate with.
     * @param userCredentials Credentials of an API-enabled user in the account.
     * @return an auth token for the user.
     */

    public String fetchLegacyAuthToken(Account account, UserCredentials userCredentials) {

        String url = buildAuthUrl(account);
        String jsonBody = gson.toJson(new UserCredentialsWrapper(userCredentials));

        Request request = RequestBuilder.newRequest().withMethod(Method.POST).withUrl(url).withBody(jsonBody).build();
        Response response = getClient(account).execute(request);
        sendRateLimitHeaders(account, response);

        return parseBasicEntity(response, stringType(), true);

    }

    /**
     * Using private app details and user credentials, attempts authentication of the user and returns their staff token
     * if they are authorised to use the app. If the credentials are invalid, an {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlServiceException}
     * will be thrown, with an error code that, in most cases, starts with "GWY".
     * @param appIdentity Details of a private app, including the Brightpearl customer account.
     * @param userCredentials Credentials of user in the account.
     * @return a staff token for the user.
     */
    public String fetchStaffToken(PrivateAppIdentity appIdentity, UserCredentials userCredentials) {
        Map<String, String> headerMap = new HashMap<String, String>();
        headerMap.put(AppAuthorisation.APP_HEADER, appIdentity.getAppReference());
        return fetchStaffToken(appIdentity.getAccount(), headerMap, userCredentials);
    }

    /**
     * Using private app details and user credentials, attempts authentication of the user and returns their staff token
     * if they are authorised to use the app. If the credentials are invalid, an {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlServiceException}
     * will be thrown, with an error code that, in most cases, starts with "GWY".
     * @param appIdentity Details of a public app.
     * @param account A Brightpearl customer's account details.
     * @param userCredentials Credentials of user in the account.
     * @return a staff token for the user.
     */
    public String fetchStaffToken(PublicAppIdentity appIdentity, Account account, UserCredentials userCredentials) {
        Map<String, String> headerMap = new HashMap<String, String>();
        headerMap.put(AppAuthorisation.DEV_HEADER, appIdentity.getDeveloperReference());
        headerMap.put(AppAuthorisation.APP_HEADER, appIdentity.getAppReference());
        return fetchStaffToken(account, headerMap, userCredentials);
    }

    /**
     * Internal method for fetching a staff token, supports both public and private.
     * @param account Brightpearl account.
     * @param headers App identification headers.
     * @param userCredentials User's credentials.
     * @return a staff token for the user.
     */
    private String fetchStaffToken(Account account, Map<String, String> headers, UserCredentials userCredentials) {
        String url = buildAuthUrl(account);
        String jsonBody = gson.toJson(new UserCredentialsWrapper(userCredentials));

        Request request = RequestBuilder.newRequest()
                .withMethod(Method.POST)
                .withUrl(url)
                .withHeaders(headers)
                .withBody(jsonBody)
                .build();

        Response response = getClient(account).execute(request);
        sendRateLimitHeaders(account, response);

        return parseBasicEntity(response, stringType(), true);

    }

    /**
     * Creates a session an app and account.
     * @return an {@link ApiSession} instance. This contains a reference to the {@link BrightpearlApiClient instance}.
     */
    public ApiSession createSession(AppAuthorisation authorisation) {
        if (authorisation == null) {
            throw new IllegalArgumentException("Authorisation details must be supplied");
        }
        return new ApiSession(authorisation);
    }

    /**
     * An authenticated API session configured with a Brightpearl customer account and headers needed to authenticate calls
     * for a single app of any type. This provides a shortcut to using the methods on the parent {@link BrightpearlApiClient}
     * class, each of which requires identity and credentials to be passed to every call.
     */
    public class ApiSession {

        private final AppAuthorisation authorisation;

        /**
         * Creates an API session authorised for calls to a single account from a single app. Supports all types.
         * @param authorisation Private app authorisation details.
         */
        private ApiSession(AppAuthorisation authorisation) {
            this.authorisation = authorisation;
        }

        /**
         * Convenience shortcut for {@link #get(ServiceReadRequest)}. Calls build on the
         * supplied builder and makes the call to the API.
         * @param serviceRequestBuilder Builder for read request to be executed.
         * @param <T> Type of expected response.
         * @return If the request was successful, a response of the expected type.
         */
        public <T> T get(ServiceReadRequestBuilder<T> serviceRequestBuilder) {
            return get(serviceRequestBuilder.build());
        }

        /**
         * Make a GET or OPTIONS call to the API, and extract the expected response entity from the JSON returned. If a valid
         * cached response is available this will be returned. Subclasses of {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlClientException} are thrown if
         * there is a failure, and other runtime exceptions may be thrown by custom implementations of {@link uk.co.visalia.brightpearl.apiclient.http.ClientFactory}.
         * @param serviceRequest A read request to be made.
         * @param <T> Type of expected response.
         * @return If the request was successful, a response of the expected type.
         */
        public <T> T get(ServiceReadRequest<T> serviceRequest) {
            return BrightpearlApiClient.this.get(authorisation, serviceRequest);
        }

        /**
         * Convenience shortcut for {@link #search(uk.co.visalia.brightpearl.apiclient.request.ServiceSearchRequest)}. Calls build on the
         * supplied builder and makes the call to the API.
         * @param serviceRequestBuilder Builder for search request to be executed.
         * @param <T> Type of expected response.
         * @return If the request was successful, a {@link SearchResults} object containing parsed results and meta information.
         */
        public <T> SearchResults<T> search(SearchRequestBuilder<T> serviceRequestBuilder) {
            return search(serviceRequestBuilder.build());
        }

        /**
         * <p>
         * Makes a GET call to a search API, and deserialises the response into a {@link SearchResults} object that contains
         * meta information, reference data and a list of results.
         * </p><p>
         * If a valid cached response is available this will be returned. Transport errors and other request failures are
         * thrown as subclasses of {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlClientException} unless a custom HTTP client or rate limiter implementation
         * throws a different type.
         * </p><p>
         * This method uses the column names returned in the search metadata to populate objects of the requested type with
         * values from each array in the results. Therefore the expected response type declared in the search request should
         * be a flat object (i.e. only primitives, Strings and Dates) with fields either named or annotated with
         * {@link com.google.gson.annotations.SerializedName} according to the column names expected. In many cases, the
         * more complex type used for resource GETs will be unsuitable for use with the corresponding resource search.
         * </p>
         * @param serviceRequest The search request to be executed.
         * @param <T> Type of expected response.
         * @return If the request was successful, a {@link SearchResults} object containing parsed results and meta information.
         */
        public <T> SearchResults<T> search(ServiceSearchRequest<T> serviceRequest) {
            return BrightpearlApiClient.this.search(authorisation, serviceRequest);
        }

        /**
         * Convenience shortcut for {@link #execute(uk.co.visalia.brightpearl.apiclient.request.ServiceWriteRequest)}. Calls build on the supplied builder and makes
         * the call to the API.
         * @param serviceRequestBuilder Builder for write request to be executed.
         * @param <T> Type of expected response. Use {@link Void}.class if no response body is expected.
         * @return If the request was successful, a response of the expected type, null if the declared type was {@link Void}.
         */
        public <T> T execute(ServiceWriteRequestBuilder<T> serviceRequestBuilder) {
            return execute(serviceRequestBuilder.build());
        }

        /**
         * Make a POST, PUT or DELETE call to the API. Subclasses of {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlClientException} are thrown if there
         * is a failure, and other runtime exceptions may be thrown by custom implementations of {@link uk.co.visalia.brightpearl.apiclient.http.ClientFactory}.
         * @param serviceRequest A write request to be made.
         * @param <T> Type of expected response. Use {@link Void}.class if no response body is expected.
         * @return If the request was successful, a response of the expected type, null if the declared type was {@link Void}.
         */
        public <T> T execute(ServiceWriteRequest<T> serviceRequest) {
            return BrightpearlApiClient.this.execute(authorisation, serviceRequest);
        }

        /**
         * Convenience method equivalent to {@link #execute(MultiRequest)}, this calls {@link MultiRequestBuilder#build()}
         * on the supplied builder for executing the request.
         * @param multiRequestBuilder Multimessage request builder to be executed.
         * @return a {@link MultiResponse} object containing the results.
         */
        public MultiResponse execute(MultiRequestBuilder multiRequestBuilder) {
            return execute(multiRequestBuilder.build());
        }

        /**
         * <p>
         * Executes a multi message request. Multi-messages can decrease latency when sending multiple write requests, and
         * count as only one request for rate limiting purposes. Use {@link MultiRequestBuilder} to assemble a multi message.
         * </p><p>
         * Errors executing the multi message request itself are thrown as exceptions from this method and generally indicate
         * that none of the individual requests have been executed by Brightpearl, though this cannot be guaranteed. For
         * example if the connection was broken after the request was sent, the result cannot be recovered. Errors in the
         * responses for individual requests are stored in the {@link uk.co.visalia.brightpearl.apiclient.multimessage.ServiceResponse} corresponding to the request, so they
         * can be handled individually.
         * </p><p>
         * The Brightpearl multi message API supports sending messages containing between two and ten and individual requests.
         * This client simplifies the use of this API by supporting any number of individual requests. If the supplied multi
         * message wrapper contains only one request it will be sent direct to the relevant API, and if the wrapper contains
         * more than ten requests it will be split into batches of ten and executed in serial. In both cases this method
         * attempts to mimic the behaviour that would be expected if all the requests had been sent in one multi-message.
         * However there are limitations to be aware of.
         * </p><p>
         * <b>Single request</b>: If the {@link MultiRequest} supplied contains only one request, it is sent direct to the
         * relevant API. HTTP transport errors are thrown by this method, and the response from the Brightpearl API is
         * returned in a mocked up {@link MultiResponse} wrapper with the wrapper status set to 200 if the individual
         * response had status 200, and 207 otherwise.
         * </p><p>
         * <b>More than ten requests</b>: The requests are split up into batches of between two and ten (batches of one are
         * avoided for consistency) and sent to the multi message API in sequence. HTTP transport exceptions will only be
         * thrown if the first batch fails with these errors; if a later batch fails with a transport error that batch and
         * all following ones will be aborted (regardless of the {@link OnFailOption}) and their request IDs included in
         * the unprocessedRequestIds field of the response. This is done so that the results of any successful batches can
         * be returned and inspected, and the list of unprocessed IDs handled, but it doesn't match the behaviour of the
         * Brightpearl API. If the status code of a batch is not 200, and the {@link OnFailOption} is {@link OnFailOption#STOP},
         * later batches are not sent, otherwise all batches will be run barring transport errors.
         * </p><p>
         * A batch of between two and ten requests is guaranteed to be sent as a single multi message, and therefore will
         * avoid behaviour that may not exactly match that provided by Brightpearl. Clients that have complex error recovery
         * requirements should avoid sending less than two or more than ten requests. The {@link uk.co.visalia.brightpearl.apiclient.multimessage.MultiMessageUtils#split(MultiRequest)}
         * utility may be used to split a request into batches.
         * </p>
         * @param multiRequest Multimessage request to be executed.
         * @return a {@link MultiResponse} object containing the results.
         */
        public MultiResponse execute(MultiRequest multiRequest) {
            return BrightpearlApiClient.this.execute(authorisation, multiRequest);
        }

        /**
         * <p>
         * Executes a request and returns the response as an unparsed string of JSON together with the HTTP status
         * code and headers, exactly as returned by the {@link uk.co.visalia.brightpearl.apiclient.http.Client} implementation.
         * This method bypasses the detection of failure response codes and error messages in the response body provided
         * by other methods, so does not throw specific exceptions such as {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlAuthException}.
         * Rate limiting is supported.
         * </p><p>
         * This method may be used to execute all types of request.
         * </p><p>
         * An exception will only be thrown for HTTP transport errors, which will usually result in a {@link uk.co.visalia.brightpearl.apiclient.exception.BrightpearlClientException}
         * unless a custom HTTP client implementation throws a different type.
         * </p>
         * @param serviceRequest Request to be sent.
         * @return Unprocessed HTTP response.
         */
        public Response getHttpResponse(AbstractServiceRequest serviceRequest) {
            return BrightpearlApiClient.this.getHttpResponse(authorisation, serviceRequest);
        }

    }

    /**
     * Convenience method equivalent to {@link #execute(AppAuthorisation, MultiRequest)}, this calls {@link MultiRequestBuilder#build()}
     * on the supplied builder for executing the request.
     * @param authorisation App and account authorisation details.
     * @param multiRequestBuilder Multimessage request builder to be executed.
     * @return a {@link MultiResponse} object containing the results.
     */
    public MultiResponse execute(AppAuthorisation authorisation, MultiRequestBuilder multiRequestBuilder) {
        return execute(authorisation, multiRequestBuilder.build());
    }

    /**
     * <p>
     * Executes a multi message request. Multi-messages can decrease latency when sending multiple write requests, and
     * count as only one request for rate limiting purposes. Use {@link MultiRequestBuilder} to assemble a multi message.
     * </p><p>
     * Errors executing the multi message request itself are thrown as exceptions from this method and generally indicate
     * that none of the individual requests have been executed by Brightpearl, though this cannot be guaranteed. For
     * example if the connection was broken after the request was sent, the result cannot be recovered. Errors in the
     * responses for individual requests are stored in the {@link uk.co.visalia.brightpearl.apiclient.multimessage.ServiceResponse} corresponding to the request, so they
     * can be handled individually.
     * </p><p>
     * The Brightpearl multi message API supports sending messages containing between two and ten and individual requests.
     * This client simplifies the use of this API by supporting any number of individual requests. If the supplied multi
     * message wrapper contains only one request it will be sent direct to the relevant API, and if the wrapper contains
     * more than ten requests it will be split into batches of ten and executed in serial. In both cases this method
     * attempts to mimic the behaviour that would be expected if all the requests had been sent in one multi-message.
     * However there are limitations to be aware of.
     * </p><p>
     * <b>Single request</b>: If the {@link MultiRequest} supplied contains only one request, it is sent direct to the
     * relevant API. HTTP transport errors are thrown by this method, and the response from the Brightpearl API is
     * returned in a mocked up {@link MultiResponse} wrapper with the wrapper status set to 200 if the individual
     * response had status 200, and 207 otherwise.
     * </p><p>
     * <b>More than ten requests</b>: The requests are split up into batches of between two and ten (batches of one are
     * avoided for consistency) and sent to the multi message API in sequence. HTTP transport exceptions will only be
     * thrown if the first batch fails with these errors; if a later batch fails with a transport error that batch and
     * all following ones will be aborted (regardless of the {@link OnFailOption}) and their request IDs included in
     * the unprocessedRequestIds field of the response. This is done so that the results of any successful batches can
     * be returned and inspected, and the list of unprocessed IDs handled, but it doesn't match the behaviour of the
     * Brightpearl API. If the status code of a batch is not 200, and the {@link OnFailOption} is {@link OnFailOption#STOP},
     * later batches are not sent, otherwise all batches will be run barring transport errors.
     * </p><p>
     * A batch of between two and ten requests is guaranteed to be sent as a single multi message, and therefore will
     * avoid behaviour that may not exactly match that provided by Brightpearl. Clients that have complex error recovery
     * requirements should avoid sending less than two or more than ten requests. The {@link uk.co.visalia.brightpearl.apiclient.multimessage.MultiMessageUtils#split(MultiRequest)}
     * utility may be used to split a request into batches.
     * </p>
     * @param authorisation App and account authorisation details.
     * @param multiRequest Multimessage request to be executed.
     * @return a {@link MultiResponse} object containing the results.
     */
    public MultiResponse execute(AppAuthorisation authorisation, MultiRequest multiRequest) {

        if (multiRequest.getRequests().size() == 0) {

            // Allow zero size batches in case clients forget to count before sending one.
            return MultiResponseBuilder.newMultiResponse().withStatus(200).build();

        } else if (multiRequest.getRequests().size() == 1) {

            // MM API doesn't support single requests so if the list contains only one request,
            // divert it to the direct API and wrap the response to a best approximation of how
            // it would have looked from the MM API.

            Map<String, ServiceResponse> serviceResponseMap = new HashMap<String, ServiceResponse>();

            ServiceWriteRequest itemRequest = multiRequest.getRequests().get(0);
            Response response = getHttpResponse(authorisation, itemRequest);

            try {
                JsonWrapper jsonWrapper = parseJsonWrapper(response, itemRequest.getResponseType(), false);