import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.sql.DataSource;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import liquibase.integration.spring.SpringLiquibase;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;
import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import ru.micord.ervu.account_applications.deserializer.DataWrapperDeserializer;
import ru.micord.ervu.account_applications.deserializer.ReferenceEntityDeserializer;
import ru.micord.ervu.account_applications.model.DataWrapper;
import ru.micord.ervu.account_applications.model.ReferenceEntity;

/**
 * Root application context
 * This context imports XML configs from all the other jars, and is created by {@link WebAppInitializer}
 * NB: modules are excluded from component scan since spring-context.xml sometimes holds important parameters and / or annotations
 * @author krylov
 */
@Configuration
@ComponentScan(basePackages = {
    "service",
    "dao",
    "bpmn",
    "i18n",
    "errorhandling",
    "database",
    "component.addresses",
    "gen",
    "ru.cg",
    "ru.micord",
    "error_handler"
}, excludeFilters = {
    @ComponentScan.Filter(type = FilterType.REGEX, pattern = "ru.cg.webbpm.modules.database.impl.DatabaseConfiguration"),
    @ComponentScan.Filter(type = FilterType.REGEX, pattern = "security.WebSecurityConfig"),
    @ComponentScan.Filter(type = FilterType.REGEX, pattern = "service.email.EmailConfig"),
})
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableWebMvc
@EnableScheduling
public class AppConfig {
  private static final Logger LOGGER = LoggerFactory.getLogger(AppConfig.class);
  @Value("${ervu.http.connectTimeout:5000}")
  private int connectTimeout;
  @Value("${ervu.http.readTimeout:15000}")
  private int readTimeout;
  @Value("${ervu.http.connectionRequestTimeout:5000}")
  private int connectionRequestTimeout;
  @Bean
  public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
    return new PropertySourcesPlaceholderConfigurer();
  }

  @Bean
  public ScheduledLockConfiguration taskScheduler(LockProvider lockProvider) {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(12);
    scheduler.initialize();
    return ScheduledLockConfigurationBuilder
        .withLockProvider(lockProvider)
        .withTaskScheduler(scheduler)
        .withDefaultLockAtMostFor(Duration.ofHours(4))
        .build();
  }

  @Bean
  public LockProvider lockProvider(@Qualifier("datasource") DataSource dataSource) {
    return new JdbcTemplateLockProvider(dataSource);
  }

  @Bean
  public SpringLiquibase liquibase(@Qualifier("datasource") DataSource dataSource) {
    SpringLiquibase liquibase = new SpringLiquibase();
    liquibase.setDataSource(dataSource);
    liquibase.setChangeLog("classpath:config/changelog-master.xml");
    return liquibase;
  }

  @Bean
  public RestTemplate restTemplate() {
    RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(connectTimeout)
        .setSocketTimeout(readTimeout)
        .setConnectionRequestTimeout(connectionRequestTimeout)
        .build();

    CloseableHttpClient httpClient = HttpClients.custom()
        .setSSLContext(sslContext())
        .setDefaultRequestConfig(requestConfig)
        .build();

    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

    return new RestTemplate(requestFactory);
  }

  @Bean
  public SSLContext sslContext() {
    TrustManager[] trustAllCerts = new TrustManager[]{
        new X509TrustManager() {
          public X509Certificate[] getAcceptedIssuers() {
            return null;
          }

          public void checkServerTrusted(X509Certificate[] certs, String authType) {}

          public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        }
    };
    SSLContext sslContext = null;

    try {
      sslContext = SSLContext.getInstance("SSL");
      sslContext.init(null, trustAllCerts, new SecureRandom());
    }
    catch (NoSuchAlgorithmException | KeyManagementException e) {
      LOGGER.error("Failed to init SSL context", e);
    }
    return sslContext;
  }

  @Bean
  @Primary
  public ObjectMapper jacksonObjectMapper() {
    JsonFactory factory = JsonFactory.builder()
        .streamReadConstraints(
            StreamReadConstraints.builder()
                .maxStringLength(100_000_000)
                .build())
        .build();

    SimpleModule module = new SimpleModule();
    customDeserializers().forEach(module::addDeserializer);

    return new ObjectMapper(factory)
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        .configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true)
        .configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true)
        .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true)
        .registerModules(new JavaTimeModule(), module);
  }

  private Map<Class, JsonDeserializer> customDeserializers() {
    return Map.of(
        ReferenceEntity.class, new ReferenceEntityDeserializer(),
        DataWrapper.class, new DataWrapperDeserializer()
    );
  }
}
