/*
 * Copyright 2019 The OpenZipkin Authors
 *
 * 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 zipkin2.storage.kafka;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.state.HostInfo;
import org.apache.kafka.streams.state.StreamsMetadata;
import zipkin2.Call;
import zipkin2.DependencyLink;
import zipkin2.Span;
import zipkin2.codec.DependencyLinkBytesDecoder;
import zipkin2.codec.SpanBytesDecoder;
import zipkin2.storage.QueryRequest;
import zipkin2.storage.ServiceAndSpanNames;
import zipkin2.storage.SpanStore;
import zipkin2.storage.Traces;
import zipkin2.storage.kafka.internal.KafkaStoreListCall;
import zipkin2.storage.kafka.internal.KafkaStoreScatterGatherListCall;
import zipkin2.storage.kafka.internal.KafkaStoreSingleKeyListCall;
import zipkin2.storage.kafka.streams.DependencyStorageTopology;
import zipkin2.storage.kafka.streams.TraceStorageTopology;

import static zipkin2.storage.kafka.streams.DependencyStorageTopology.DEPENDENCIES_STORE_NAME;
import static zipkin2.storage.kafka.streams.TraceStorageTopology.REMOTE_SERVICE_NAMES_STORE_NAME;
import static zipkin2.storage.kafka.streams.TraceStorageTopology.SERVICE_NAMES_STORE_NAME;
import static zipkin2.storage.kafka.streams.TraceStorageTopology.SPAN_NAMES_STORE_NAME;
import static zipkin2.storage.kafka.streams.TraceStorageTopology.TRACES_STORE_NAME;

/**
 * Span store backed by Kafka Stream distributed state stores built by {@link
 * TraceStorageTopology} and {@link DependencyStorageTopology}, and made accessible by
 * {@link  KafkaStorageHttpService}.
 */
final class KafkaSpanStore implements SpanStore, Traces, ServiceAndSpanNames {
  static final ObjectMapper MAPPER = new ObjectMapper();
  final KafkaStorage storage;
  final BiFunction<String, Integer, String> httpBaseUrl;
  final boolean traceSearchEnabled, traceByIdQueryEnabled, dependencyQueryEnabled;

  KafkaSpanStore(KafkaStorage storage) {
    this.storage = storage;
    httpBaseUrl = storage.httpBaseUrl;
    traceByIdQueryEnabled = storage.traceByIdQueryEnabled;
    traceSearchEnabled = storage.traceSearchEnabled;
    dependencyQueryEnabled = storage.dependencyQueryEnabled;
  }

  @Override public Call<List<List<Span>>> getTraces(QueryRequest request) {
    if (traceSearchEnabled) {
      return new GetTracesCall(storage.getTraceStorageStream(), httpBaseUrl, request);
    } else {
      return Call.emptyList();
    }
  }

  @SuppressWarnings("deprecation")
  @Override public Call<List<Span>> getTrace(String traceId) {
    if (traceByIdQueryEnabled) {
      return new GetTraceCall(storage.getTraceStorageStream(), httpBaseUrl,
          Span.normalizeTraceId(traceId));
    } else {
      return Call.emptyList();
    }
  }

  @Override public Call<List<List<Span>>> getTraces(Iterable<String> traceIds) {
    if (traceByIdQueryEnabled) {
      StringJoiner joiner = new StringJoiner(",");
      for (String traceId : traceIds) {
        joiner.add(Span.normalizeTraceId(traceId));
      }

      if (joiner.length() == 0) return Call.emptyList();
      return new GetTraceManyCall(storage.getTraceStorageStream(), httpBaseUrl, joiner.toString());
    } else {
      return Call.emptyList();
    }
  }

  @Deprecated @Override public Call<List<String>> getServiceNames() {
    if (traceSearchEnabled) {
      return new GetServiceNamesCall(storage.getTraceStorageStream(), httpBaseUrl);
    } else {
      return Call.emptyList();
    }
  }

  @Deprecated @Override public Call<List<String>> getSpanNames(String serviceName) {
    if (traceSearchEnabled) {
      return new GetSpanNamesCall(storage.getTraceStorageStream(), serviceName, httpBaseUrl);
    } else {
      return Call.emptyList();
    }
  }

  @Override public Call<List<String>> getRemoteServiceNames(String serviceName) {
    if (traceSearchEnabled) {
      return new GetRemoteServiceNamesCall(storage.getTraceStorageStream(), serviceName, httpBaseUrl);
    } else {
      return Call.emptyList();
    }
  }

  @Override public Call<List<DependencyLink>> getDependencies(long endTs, long lookback) {
    if (dependencyQueryEnabled) {
      return new GetDependenciesCall(storage.getDependencyStorageStream(), httpBaseUrl, endTs, lookback);
    } else {
      return Call.emptyList();
    }
  }

  static final class GetServiceNamesCall extends KafkaStoreScatterGatherListCall<String> {
    static final long SERVICE_NAMES_LIMIT = 1_000;
    final KafkaStreams traceStoreStream;
    final BiFunction<String, Integer, String> httpBaseUrl;

    GetServiceNamesCall(KafkaStreams traceStoreStream,
        BiFunction<String, Integer, String> httpBaseUrl) {
      super(
          traceStoreStream,
          SERVICE_NAMES_STORE_NAME,
          httpBaseUrl,
          "/serviceNames",
          SERVICE_NAMES_LIMIT);
      this.traceStoreStream = traceStoreStream;
      this.httpBaseUrl = httpBaseUrl;
    }

    @Override protected String parseItem(JsonNode node) {
      return node.textValue();
    }

    @Override public Call<List<String>> clone() {
      return new GetServiceNamesCall(traceStoreStream, httpBaseUrl);
    }
  }

  static final class GetSpanNamesCall extends KafkaStoreSingleKeyListCall<String> {
    final KafkaStreams traceStoreStream;
    final String serviceName;
    final BiFunction<String, Integer, String> httpBaseUrl;

    GetSpanNamesCall(KafkaStreams traceStoreStream, String serviceName,
        BiFunction<String, Integer, String> httpBaseUrl) {
      super(traceStoreStream, SPAN_NAMES_STORE_NAME, httpBaseUrl,
          "/serviceNames/" + serviceName + "/spanNames", serviceName);
      this.traceStoreStream = traceStoreStream;
      this.serviceName = serviceName;
      this.httpBaseUrl = httpBaseUrl;
    }

    @Override protected String parseItem(JsonNode node) {
      return node.textValue();
    }

    @Override public Call<List<String>> clone() {
      return new GetSpanNamesCall(traceStoreStream, serviceName, httpBaseUrl);
    }
  }

  static final class GetRemoteServiceNamesCall extends KafkaStoreSingleKeyListCall<String> {
    final KafkaStreams traceStoreStream;
    final String serviceName;
    final BiFunction<String, Integer, String> httpBaseUrl;

    GetRemoteServiceNamesCall(KafkaStreams traceStoreStream, String serviceName,
        BiFunction<String, Integer, String> httpBaseUrl) {
      super(traceStoreStream, REMOTE_SERVICE_NAMES_STORE_NAME, httpBaseUrl,
          "/serviceNames/" + serviceName + "/remoteServiceNames", serviceName);
      this.traceStoreStream = traceStoreStream;
      this.serviceName = serviceName;
      this.httpBaseUrl = httpBaseUrl;
    }

    @Override protected String parseItem(JsonNode node) {
      return node.textValue();
    }

    @Override public Call<List<String>> clone() {
      return new GetRemoteServiceNamesCall(traceStoreStream, serviceName, httpBaseUrl);
    }
  }

  static final class GetTracesCall extends KafkaStoreScatterGatherListCall<List<Span>> {
    final KafkaStreams traceStoreStream;
    final BiFunction<String, Integer, String> httpBaseUrl;
    final QueryRequest request;

    GetTracesCall(KafkaStreams traceStoreStream,
        BiFunction<String, Integer, String> httpBaseUrl,
        QueryRequest request) {
      super(
          traceStoreStream,
          TRACES_STORE_NAME,
          httpBaseUrl,
          ("/traces?"
              + (request.serviceName() == null ? "" : "serviceName=" + request.serviceName() + "&")
              + (request.remoteServiceName() == null ? ""
              : "remoteServiceName=" + request.remoteServiceName() + "&")
              + (request.spanName() == null ? "" : "spanName=" + request.spanName() + "&")
              + (request.annotationQueryString() == null ? ""
              : "annotationQuery=" + request.annotationQueryString() + "&")
              + (request.minDuration() == null ? "" : "minDuration=" + request.minDuration() + "&")
              + (request.maxDuration() == null ? "" : "maxDuration=" + request.maxDuration() + "&")
              + ("endTs=" + request.endTs() + "&")
              + ("lookback=" + request.lookback() + "&")
              + ("limit=" + request.limit())),
          request.limit());
      this.traceStoreStream = traceStoreStream;
      this.httpBaseUrl = httpBaseUrl;
      this.request = request;
    }

    @Override protected List<Span> parseItem(JsonNode node) throws JsonProcessingException {
      return SpanBytesDecoder.JSON_V2.decodeList(MAPPER.writeValueAsBytes(node));
    }

    @Override public Call<List<List<Span>>> clone() {
      return new GetTracesCall(traceStoreStream, httpBaseUrl, request);
    }
  }

  static final class GetTraceCall extends KafkaStoreSingleKeyListCall<Span> {
    final KafkaStreams traceStoreStream;
    final BiFunction<String, Integer, String> httpBaseUrl;
    final String traceId;

    GetTraceCall(KafkaStreams traceStoreStream,
        BiFunction<String, Integer, String> httpBaseUrl,
        String traceId) {
      super(traceStoreStream, TRACES_STORE_NAME, httpBaseUrl, String.format("/traces/%s", traceId),
          traceId);
      this.traceStoreStream = traceStoreStream;
      this.httpBaseUrl = httpBaseUrl;
      this.traceId = traceId;
    }

    @Override protected Span parseItem(JsonNode node) throws JsonProcessingException {
      return SpanBytesDecoder.JSON_V2.decodeOne(MAPPER.writeValueAsBytes(node));
    }

    @Override public Call<List<Span>> clone() {
      return new GetTraceCall(traceStoreStream, httpBaseUrl, traceId);
    }
  }

  static final class GetTraceManyCall extends KafkaStoreListCall<List<Span>> {
    static final StringSerializer STRING_SERIALIZER = new StringSerializer();

    final KafkaStreams traceStoreStream;
    final BiFunction<String, Integer, String> httpBaseUrl;
    final String traceIds;

    GetTraceManyCall(KafkaStreams traceStoreStream,
        BiFunction<String, Integer, String> httpBaseUrl,
        String traceIds) {
      super(traceStoreStream, TRACES_STORE_NAME, httpBaseUrl, "/traceMany?traceIds=" + traceIds);
      this.traceStoreStream = traceStoreStream;
      this.httpBaseUrl = httpBaseUrl;
      this.traceIds = traceIds;
    }

    @Override protected List<Span> parseItem(JsonNode node) throws JsonProcessingException {
      return SpanBytesDecoder.JSON_V2.decodeList(MAPPER.writeValueAsBytes(node));
    }

    @Override public Call<List<List<Span>>> clone() {
      return new GetTraceManyCall(traceStoreStream, httpBaseUrl, traceIds);
    }

    @Override
    protected CompletableFuture<List<List<Span>>> listFuture() {
      // To reduce calls to store instances traceIds are grouped by hostInfo
      Map<HostInfo, List<String>> traceIdsByHost = new LinkedHashMap<>();
      for (String traceId : traceIds.split(",", 1_000)) {
        StreamsMetadata metadata =
            traceStoreStream.metadataForKey(TRACES_STORE_NAME, traceId, STRING_SERIALIZER);
        List<String> collected = traceIdsByHost.get(metadata.hostInfo());
        if (collected == null) collected = new ArrayList<>();
        collected.add(traceId);
        traceIdsByHost.put(metadata.hostInfo(), collected);
      }
      // Only calls to hosts that have traceIds are executed
      List<CompletableFuture<AggregatedHttpResponse>> responseFutures =
          traceIdsByHost.entrySet()
              .stream()
              .map(entry -> httpClient(entry.getKey())
                  .get("/traceMany?traceIds=" + String.join(",", entry.getValue())))
              .map(HttpResponse::aggregate)
              .collect(Collectors.toList());
      return CompletableFuture.allOf(responseFutures.toArray(new CompletableFuture[0]))
          .thenApply(unused ->
              responseFutures.stream()
                  .map(s -> s.getNow(AggregatedHttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR)))
                  .map(this::content)
                  .map(this::parseList)
                  .flatMap(Collection::stream)
                  .distinct()
                  .collect(Collectors.toList()));
    }
  }

  static final class GetDependenciesCall extends KafkaStoreScatterGatherListCall<DependencyLink> {
    static final long DEPENDENCIES_LIMIT = 1_000;

    final KafkaStreams dependencyStoreStream;
    final BiFunction<String, Integer, String> httpBaseUrl;
    final long endTs, lookback;

    GetDependenciesCall(KafkaStreams dependencyStoreStream,
        BiFunction<String, Integer, String> httpBaseUrl,
        long endTs, long lookback) {
      super(
          dependencyStoreStream,
          DEPENDENCIES_STORE_NAME,
          httpBaseUrl,
          "/dependencies?endTs=" + endTs + "&lookback=" + lookback,
          DEPENDENCIES_LIMIT);
      this.dependencyStoreStream = dependencyStoreStream;
      this.httpBaseUrl = httpBaseUrl;
      this.endTs = endTs;
      this.lookback = lookback;
    }

    @Override protected DependencyLink parseItem(JsonNode node) throws JsonProcessingException {
      return DependencyLinkBytesDecoder.JSON_V1.decodeOne(MAPPER.writeValueAsBytes(node));
    }

    @Override public Call<List<DependencyLink>> clone() {
      return new GetDependenciesCall(dependencyStoreStream, httpBaseUrl, endTs, lookback);
    }
  }
}