package net.ttddyy.evernote.rest;

import com.evernote.auth.EvernoteService;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.web.SpringBootServletInitializer;
import org.springframework.context.annotation.*;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.social.evernote.api.Evernote;
import org.springframework.social.evernote.api.impl.EvernoteTemplate;
import org.springframework.social.evernote.connect.EvernoteConnectionFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.WebRequest;

import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;

/**
 * Evernote Rest Webapp application and configuration.
 *
 * @author Tadaya Tsuyukubo
 */
@ComponentScan
@Configuration
@EnableAutoConfiguration
@EnableConfigurationProperties
public class Application {

	@Autowired
	public EvernotePropertiesConfiguration evernotePropertiesConfiguration;


	@Configuration
	@ConfigurationProperties("evernote")
	public static class EvernotePropertiesConfiguration {

		@NotNull
		public String consumerKey;
		@NotNull
		public String consumerSecret;

		public String accessToken;
		public boolean alwaysUseTokenFromConfig;
		public boolean fallbackToTokenFromConfig;
		public EvernoteService environment = EvernoteService.SANDBOX;  // default is sandbox
		public String userAgent;
		public Map<String, String> customHeaders = new HashMap<String, String>();

		public void setEnvironment(EvernoteService environment) {
			this.environment = environment;
		}

		public void setConsumerKey(String consumerKey) {
			this.consumerKey = consumerKey;
		}

		public void setConsumerSecret(String consumerSecret) {
			this.consumerSecret = consumerSecret;
		}

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

		public void setAlwaysUseTokenFromConfig(boolean alwaysUseTokenFromConfig) {
			this.alwaysUseTokenFromConfig = alwaysUseTokenFromConfig;
		}

		public void setFallbackToTokenFromConfig(boolean fallbackToTokenFromConfig) {
			this.fallbackToTokenFromConfig = fallbackToTokenFromConfig;
		}

		public void setUserAgent(String userAgent) {
			this.userAgent = userAgent;
		}

		public void setCustomHeaders(Map<String, String> customHeaders) {
			this.customHeaders = customHeaders;
		}

		// getter is required for map to bind property values
		public Map<String, String> getCustomHeaders() {
			return customHeaders;
		}

	}


	@Bean
	public EvernoteConnectionFactory evernoteConnectionFactory() {
		final String consumerKey = this.evernotePropertiesConfiguration.consumerKey;
		final String consumerSecret = this.evernotePropertiesConfiguration.consumerSecret;
		final EvernoteService evernoteService = this.evernotePropertiesConfiguration.environment;
		return new EvernoteConnectionFactory(consumerKey, consumerSecret, evernoteService);
	}

	@Bean
	@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES)
	public Evernote evernote(WebRequest request) {
		final EvernotePropertiesConfiguration config = this.evernotePropertiesConfiguration;
		final EvernoteService evernoteService = config.environment;

		final Evernote evernote;
		if (config.alwaysUseTokenFromConfig) {
			evernote = new EvernoteTemplate(evernoteService, config.accessToken);
		} else {
			String accessToken = request.getHeader("evernote-rest-accesstoken");
			if (accessToken == null && config.fallbackToTokenFromConfig) {
				accessToken = config.accessToken; // fallback to accesstoken from config
			}

			final String noteStoreUrl = request.getHeader("evernote-rest-notestoreurl");
			final String webApiUrlPrefix = request.getHeader("evernote-rest-webapiurlprefix");
			final String userId = request.getHeader("evernote-rest-userid");

			if (noteStoreUrl != null && webApiUrlPrefix != null && userId != null) {
				evernote = new EvernoteTemplate(evernoteService, accessToken, noteStoreUrl, webApiUrlPrefix, userId);
			} else {
				evernote = new EvernoteTemplate(evernoteService, accessToken);
			}
		}

		// for this rest app, do not create proxy for thrift object
		evernote.setApplyNullSafe(false);

		if (config.userAgent != null) {
			evernote.clientFactory().setUserAgent(config.userAgent);
		}

		final Map<String, String> customHeaders = config.getCustomHeaders();
		if (!customHeaders.isEmpty()) {
			evernote.clientFactory().setCustomHeaders(customHeaders);
		}


		return evernote;
	}

	/**
	 * override spring-boot default ObjectMapper to configure output(serialization) json.
	 *
	 * @see org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration.ObjectMappers#jacksonObjectMapper()
	 */
	@Bean
	public ObjectMapper jacksonObjectMapper() {

		// use different visibility for serialization(output json)
		// I want to ONLY change the visibility for serialization, but couldn't find nice way to do it.
		// maybe related to this issue: https://github.com/FasterXML/jackson-databind/issues/352
		// for now, override ObjectMapper and set new SerializationConfig in instance initializer.
		// TODO: find correct way to do this.
		final ObjectMapper mapper = new ObjectMapper() {
			{
				// use instance fields for output json
				_serializationConfig = _serializationConfig.with(
						_serializationConfig.getDefaultVisibilityChecker()
								.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
								.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)
								.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
								.withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
								.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
				);
			}

		};

		// mix-in to ignore thrift specific fields for serialization.
		mapper.addMixInAnnotations(Object.class, ThriftPropertyJacksonFilter.class);

		return mapper;
	}

	/**
	 * Mix-in class for Jackson to ignore thrift specific fields in serialization.
	 */
	@JsonIgnoreProperties("__isset_vector")
	private static abstract class ThriftPropertyJacksonFilter {
	}

	/**
	 * To maximize the caching feature in name discoverer, inject as a bean instead of creating every time.
	 */
	@Bean
	public ParameterNameDiscoverer parameterNameDiscoverer() {
		return new DefaultParameterNameDiscoverer();
	}

	@Bean
	public ParameterJavaTypeDiscoverer parameterJavaTypeDiscoverer() {
		return new ParameterJavaTypeDiscoverer();
	}

	public static void main(String[] args) throws Exception {
		SpringApplication.run(Application.class, args);
	}

	/**
	 * To initialize application when it is deployed to web container as a war file.
	 */
	public static class ApplicationServletInitializer extends SpringBootServletInitializer {
		@Override
		protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
			return application.sources(Application.class);
		}
	}

}