package org.zalando.fahrschein.example;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import okhttp3.CertificatePinner;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.ConnectionConfig;
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.http.client.OkHttp3ClientHttpRequestFactory;
import org.zalando.fahrschein.*;
import org.zalando.fahrschein.domain.Cursor;
import org.zalando.fahrschein.domain.Lock;
import org.zalando.fahrschein.domain.Partition;
import org.zalando.fahrschein.domain.Subscription;
import org.zalando.fahrschein.example.domain.OrderEvent;
import org.zalando.fahrschein.example.domain.OrderEventProcessor;
import org.zalando.fahrschein.example.domain.SalesOrder;
import org.zalando.fahrschein.example.domain.SalesOrderPlaced;
import org.zalando.fahrschein.http.apache.HttpComponentsRequestFactory;
import org.zalando.fahrschein.http.api.RequestFactory;
import org.zalando.fahrschein.http.spring.SpringRequestFactory;
import org.zalando.fahrschein.inmemory.InMemoryCursorManager;
import org.zalando.fahrschein.jdbc.JdbcCursorManager;
import org.zalando.fahrschein.jdbc.JdbcPartitionManager;
import org.zalando.jackson.datatype.money.MoneyModule;

import javax.sql.DataSource;
import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static java.util.Arrays.asList;
import static org.zalando.fahrschein.AuthorizationBuilder.authorization;
import static org.zalando.fahrschein.domain.Authorization.AuthorizationAttribute.ANYONE;

public class Main {

    private static final Logger LOG = LoggerFactory.getLogger(Main.class);
    private static final String SALES_ORDER_SERVICE_ORDER_PLACED = "sales-order-service.order-placed";
    private static final URI NAKADI_URI = URI.create("https://nakadi-staging.aruha-test.zalan.do");
    private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/local_nakadi_cursor_db";
    private static final String JDBC_USERNAME = "postgres";
    private static final String JDBC_PASSWORD = "postgres";
    public static final String ORDER_CREATED = "eventlog.e96001_order_created";
    public static final String ORDER_PAYMENT_STATUS_ACCEPTED = "eventlog.e62001_order_payment_status_accepted";

    public static void main(String[] args) throws IOException, InterruptedException {

        final ObjectMapper objectMapper = new ObjectMapper();

        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);

        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.registerModule(new Jdk8Module());
        objectMapper.registerModule(new MoneyModule());
        objectMapper.registerModule(new ParameterNamesModule());

        final Listener<SalesOrderPlaced> listener = events -> {
            if (Math.random() < 0.0000001) {
                // For testing reconnection logic
                throw new EventProcessingException("Random failure");
            } else {
                for (SalesOrderPlaced salesOrderPlaced : events) {
                    final SalesOrder order = salesOrderPlaced.getSalesOrder();
                    LOG.info("Received sales order [{}] created at [{}]", order.getOrderNumber(), order.getCreatedAt());
                }
            }
        };

        //subscriptionListen(objectMapper, listener);

        subscriptionListenHttpComponents(objectMapper, listener);

        //subscriptionListenSpringAdapter(objectMapper, listener);

        //subscriptionListenWithPositionCursors(objectMapper, listener);

        //subscriptionMultipleEvents(objectMapper);

        //simpleListen(objectMapper, listener);

        //persistentListen(objectMapper, listener);

        //subscriptionCreateWithAuthorization(objectMapper, listener);

        //multiInstanceListen(objectMapper, listener);
    }

    private static void subscriptionMultipleEvents(ObjectMapper objectMapper) throws IOException {
        final OrderEventProcessor processor = new OrderEventProcessor();

        final Listener<OrderEvent> listener = events -> {
            for (OrderEvent event: events) {
                event.process(processor);
            }
        };

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Set<String> events = new HashSet<>(asList(ORDER_CREATED, ORDER_PAYMENT_STATUS_ACCEPTED));

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", events)
                .withConsumerGroup("fahrschein-demo")
                .readFromEnd()
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(OrderEvent.class, listener);
    }

    private static void subscriptionListenWithPositionCursors(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {

        final List<Cursor> cursors = asList(
                new Cursor("0", "000000000000109993", "sales-order-service.order-placed"),
                new Cursor("1", "000000000000110085", "sales-order-service.order-placed"),
                new Cursor("2", "000000000000109128", "sales-order-service.order-placed"),
                new Cursor("3", "000000000000110205", "sales-order-service.order-placed"),
                new Cursor("4", "000000000000109161", "sales-order-service.order-placed"),
                new Cursor("5", "000000000000109087", "sales-order-service.order-placed"),
                new Cursor("6", "000000000000109100", "sales-order-service.order-placed"),
                new Cursor("7", "000000000000109146", "sales-order-service.order-placed"));

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", SALES_ORDER_SERVICE_ORDER_PLACED)
                .withConsumerGroup("fahrschein-demo")
                .readFromCursors(cursors)
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void subscriptionListen(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", SALES_ORDER_SERVICE_ORDER_PLACED)
                .withConsumerGroup("fahrschein-demo")
                .readFromEnd()
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void subscriptionListenHttpComponents(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {
        final RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(60000)
                .setConnectTimeout(2000)
                .setConnectionRequestTimeout(8000)
                .setContentCompressionEnabled(false)
                .build();

        final ConnectionConfig connectionConfig = ConnectionConfig.custom()
                .setBufferSize(512)
                .build();

        final CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setDefaultConnectionConfig(connectionConfig)
                .setConnectionTimeToLive(30, TimeUnit.SECONDS)
                .disableAutomaticRetries()
                .disableRedirectHandling()
                .setMaxConnTotal(8)
                .setMaxConnPerRoute(2)
                .build();

        final RequestFactory requestFactory = new HttpComponentsRequestFactory(httpClient);

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withRequestFactory(requestFactory)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", SALES_ORDER_SERVICE_ORDER_PLACED)
                .withConsumerGroup("fahrschein-demo")
                .readFromEnd()
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void subscriptionListenSpringAdapter(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {

        final OkHttpClient client = new OkHttpClient.Builder()
                .readTimeout(60, TimeUnit.SECONDS)
                .connectTimeout(2, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .connectionPool(new ConnectionPool(2, 5*60, TimeUnit.SECONDS))
                .certificatePinner(new CertificatePinner.Builder()
                        .add(NAKADI_URI.getHost(), "sha256/KMUmME9xy7BKVUZ80VcmQ75zIZo16IZRTqVRYHVZeWY=")
                        .build())
                .build();

        final OkHttp3ClientHttpRequestFactory clientHttpRequestFactory = new OkHttp3ClientHttpRequestFactory(client);
        final SpringRequestFactory requestFactory = new SpringRequestFactory(clientHttpRequestFactory);

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withRequestFactory(requestFactory)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", SALES_ORDER_SERVICE_ORDER_PLACED)
                .withConsumerGroup("fahrschein-demo")
                .readFromEnd()
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }


    private static void simpleListen(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {
        final InMemoryCursorManager cursorManager = new InMemoryCursorManager();

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .withCursorManager(cursorManager)
                .build();

        final List<Partition> partitions = nakadiClient.getPartitions(SALES_ORDER_SERVICE_ORDER_PLACED);

        nakadiClient.stream(SALES_ORDER_SERVICE_ORDER_PLACED)
                .readFromBegin(partitions)
                .withObjectMapper(objectMapper)
                .withBackoffStrategy(new EqualJitterBackoffStrategy().withMaxRetries(10))
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void persistentListen(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {
        final HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(JDBC_URL);
        hikariConfig.setUsername(JDBC_USERNAME);
        hikariConfig.setPassword(JDBC_PASSWORD);

        final DataSource dataSource = new HikariDataSource(hikariConfig);

        final JdbcCursorManager cursorManager = new JdbcCursorManager(dataSource, "fahrschein-demo");

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .withCursorManager(cursorManager)
                .build();

        final List<Partition> partitions = nakadiClient.getPartitions(SALES_ORDER_SERVICE_ORDER_PLACED);

        nakadiClient.stream(SALES_ORDER_SERVICE_ORDER_PLACED)
                .readFromBegin(partitions)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void subscriptionCreateWithAuthorization(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {

        final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                .withAccessTokenProvider(new ZignAccessTokenProvider())
                .build();

        final Subscription subscription = nakadiClient.subscription("fahrschein-demo", SALES_ORDER_SERVICE_ORDER_PLACED)
                .withConsumerGroup("fahrschein-demo")
                .withAuthorization(authorization()
                    .addAdmin("user", "you")
                    .addAdmin("user", "your_friend")
                    .addAdmin("user", "your_dog")
                    .withReaders(ANYONE)
                    .build())
                .subscribe();

        nakadiClient.stream(subscription)
                .withObjectMapper(objectMapper)
                .listen(SalesOrderPlaced.class, listener);
    }

    private static void multiInstanceListen(ObjectMapper objectMapper, Listener<SalesOrderPlaced> listener) throws IOException {
        final HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(JDBC_URL);
        hikariConfig.setUsername(JDBC_USERNAME);
        hikariConfig.setPassword(JDBC_PASSWORD);

        final DataSource dataSource = new HikariDataSource(hikariConfig);

        final ZignAccessTokenProvider accessTokenProvider = new ZignAccessTokenProvider();

        final AtomicInteger name = new AtomicInteger();
        final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(16);

        for (int i = 0; i < 12; i++) {
            final String instanceName = "consumer-" + name.getAndIncrement();
            final JdbcPartitionManager partitionManager = new JdbcPartitionManager(dataSource, "fahrschein-demo");
            final JdbcCursorManager cursorManager = new JdbcCursorManager(dataSource, "fahrschein-demo");

            final NakadiClient nakadiClient = NakadiClient.builder(NAKADI_URI)
                    .withAccessTokenProvider(accessTokenProvider)
                    .withCursorManager(cursorManager)
                    .build();

            final List<Partition> partitions = nakadiClient.getPartitions(SALES_ORDER_SERVICE_ORDER_PLACED);

            final IORunnable instance = () -> {

                final IORunnable runnable = () -> {
                    final Optional<Lock> optionalLock = partitionManager.lockPartitions(SALES_ORDER_SERVICE_ORDER_PLACED, partitions, instanceName);

                    if (optionalLock.isPresent()) {
                        final Lock lock = optionalLock.get();
                        try {
                            nakadiClient.stream(SALES_ORDER_SERVICE_ORDER_PLACED)
                                    .withLock(lock)
                                    .withObjectMapper(objectMapper)
                                    .withStreamParameters(new StreamParameters().withStreamLimit(10))
                                    .withBackoffStrategy(new NoBackoffStrategy())
                                    .listen(SalesOrderPlaced.class, listener);
                        } finally {
                            partitionManager.unlockPartitions(lock);
                        }
                    }
                };

                scheduledExecutorService.scheduleWithFixedDelay(runnable.unchecked(), 0, 1, TimeUnit.SECONDS);
            };
            scheduledExecutorService.submit(instance.unchecked());
        }

        try {
            Thread.sleep(60L*1000);
            scheduledExecutorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}