/*
 * Copyright (C) 2014 IZITEQ B.V.
 *
 * 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 travel.izi.api;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import okhttp3.*;
import retrofit2.Converter;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import travel.izi.api.service.*;
import travel.izi.api.util.MediaHelper;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("unused")
public final class IZITravel {

    public static final Server SERVER_PRODUCTION = new Server(
            "http://api.izi.travel",
            "http://media.izi.travel"
    );
    public static final Server SERVER_STAGING = new Server(
            "http://api.stage.izi.travel",
            "http://media.stage.izi.travel"
    );
    public static final Server SERVER_DEVELOPMENT = new Server(
            "http://api.dev.izi.travel",
            "http://media.dev.izi.travel"
    );

    /**
     * The default API version.
     */
    public static final String API_VERSION = "1.4";

    private final String apiKey;
    private final String apiVersion;
    private final String apiPassword;
    private final Server server;
    private final Retrofit retrofit;

    private IZITravel(String apiKey, String apiVersion, String apiPassword, Server server, OkHttpClient.Builder clientBuilder) {
        this.apiKey = apiKey;
        this.apiVersion = apiVersion;
        this.apiPassword = apiPassword;
        this.server = server;

        retrofit = new Retrofit.Builder()
                .baseUrl(server.apiUrl())
                .client(clientBuilder.addInterceptor(createApiInterceptor()).build())
                .addConverterFactory(createConverterFactory())
                .build();
    }

    private Interceptor createApiInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request originalRequest = chain.request();
                Request.Builder requestBuilder = originalRequest.newBuilder();
                requestBuilder.addHeader("Accept", String.format("application/izi-api-v%s+json", apiVersion));
                requestBuilder.addHeader("X-IZI-API-KEY", apiKey);
                if (apiPassword != null) {
                    requestBuilder.addHeader("X-IZI-API-PASSWORD", apiPassword);
                }
                requestBuilder.cacheControl(new CacheControl.Builder().maxStale(1, TimeUnit.HOURS).build());
                return chain.proceed(requestBuilder.build());
            }
        };
    }

    private Converter.Factory createConverterFactory() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return JacksonConverterFactory.create(mapper);
    }

    public CityService cityService() {
        return retrofit.create(CityService.class);
    }

    public CountryService countryService() {
        return retrofit.create(CountryService.class);
    }

    public FeaturedService featuredService() {
        return retrofit.create(FeaturedService.class);
    }

    public LanguageService languageService() {
        return retrofit.create(LanguageService.class);
    }

    public MtgObjectService mtgObjectService() {
        return retrofit.create(MtgObjectService.class);
    }

    public PublisherService publisherService() {
        return retrofit.create(PublisherService.class);
    }

    public ReviewService reviewService() {
        return retrofit.create(ReviewService.class);
    }

    public SearchService searchService() {
        return retrofit.create(SearchService.class);
    }

    public MediaHelper mediaHelper() {
        return new MediaHelper(server.mediaUrl);
    }

    /**
     * The default {@link IZITravel} instance.
     * <p>This instance is automatically initialized with defaults that are suitable to most
     * implementations.</p>
     * <ul>
     * <li>API version is {@link #API_VERSION}</li>
     * <li>Server is {@link #SERVER_PRODUCTION}</li>
     * </ul>
     * <p>If these settings do not meet the requirements of your application you can construct your own
     * with full control over the configuration by using {@link IZITravel.Builder} to create a
     * {@link IZITravel} instance.</p>
     */
    public static IZITravel withApiKey(String apiKey) {
        return new Builder().apiKey(apiKey).build();
    }

    /**
     * A helper class for creating {@link IZITravel} instances.
     */
    public static final class Builder {
        private String apiKey;
        private String apiVersion;
        private String apiPassword;
        private Server server;
        private OkHttpClient.Builder clientBuilder;

        /**
         * Set API key. All API methods require a valid API key.
         */
        public Builder apiKey(String apiKey) {
            if (apiKey == null) {
                throw new IllegalArgumentException("API key must not be null.");
            }
            this.apiKey = apiKey;
            return this;
        }

        /**
         * Set API version. By default used {@link #API_VERSION}.
         */
        public Builder apiVersion(String apiVersion) {
            if (apiVersion == null) {
                throw new IllegalArgumentException("API version must not be null.");
            }
            this.apiVersion = apiVersion;
            return this;
        }

        /**
         * Set API password. Using for getting access to "limited" content. By default available
         * only public content.
         */
        public Builder apiPassword(String apiPassword) {
            this.apiPassword = apiPassword;
            return this;
        }

        /**
         * Set {@link Server}. By default using {@link #SERVER_PRODUCTION}.
         */
        public Builder server(Server server) {
            if (server == null) {
                throw new IllegalArgumentException("Server must not be null.");
            }
            this.server = server;
            return this;
        }

        /**
         * Set the HTTP client used for requests. Optional.
         */
        public Builder client(OkHttpClient client) {
            if (client == null) {
                throw new IllegalArgumentException("Client must not be null.");
            }
            this.clientBuilder = client.newBuilder();
            return this;
        }

        /**
         * Create the {@link IZITravel} instance.
         */
        public IZITravel build() {
            if (apiKey == null) {
                throw new IllegalArgumentException("API key is not specified");
            }
            if (apiVersion == null) {
                apiVersion = API_VERSION;
            }
            if (server == null) {
                server = SERVER_PRODUCTION;
            }
            if (clientBuilder == null) {
                clientBuilder = new OkHttpClient.Builder();
            }
            return new IZITravel(apiKey, apiVersion, apiPassword, server, clientBuilder);
        }
    }

    /**
     * Deserialize an error response body to {@link Error}.
     */
    public Error parseError(ResponseBody errorBody) throws IOException {
        Converter<ResponseBody, Error> errorConverter = retrofit.responseBodyConverter(Error.class, new Annotation[0]);
        return errorConverter.convert(errorBody);
    }

    /**
     * An error returned by the API.
     */
    public static final class Error {
        private final int code;
        private final String message;

        @JsonCreator
        public Error(@JsonProperty("code") int code, @JsonProperty("error") String message) {
            this.code = code;
            this.message = message;
        }

        public int code() {
            return code;
        }

        public String message() {
            return message;
        }

        @Override
        public String toString() {
            return code + " " + message;
        }
    }

    /**
     * The server.
     */
    public static final class Server {
        private final String apiUrl;
        private final String mediaUrl;

        public Server(String apiUrl, String mediaUrl) {
            this.apiUrl = apiUrl;
            this.mediaUrl = mediaUrl;
        }

        public String apiUrl() {
            return apiUrl;
        }

        public String mediaUrl() {
            return mediaUrl;
        }
    }

}