/*
 * Copyright The OpenZipkin Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package brave.okhttp3;

import brave.Span;
import brave.Tracing;
import brave.http.HttpClientHandler;
import brave.http.HttpClientRequest;
import brave.http.HttpClientResponse;
import brave.http.HttpTracing;
import brave.internal.Nullable;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.Scope;
import brave.propagation.TraceContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import okhttp3.Connection;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

import static brave.okhttp3.TracingCallFactory.NULL_SENTINEL;

/**
 * This is a network-level interceptor, which creates a new span for each attempt. Note that this
 * does not work well for high traffic servers, as the span context can be lost when under backlog.
 * In cases like that, use {@link TracingCallFactory}.
 */
public final class TracingInterceptor implements Interceptor {
  public static Interceptor create(Tracing tracing) {
    return create(HttpTracing.create(tracing));
  }

  public static Interceptor create(HttpTracing httpTracing) {
    return new TracingInterceptor(httpTracing);
  }

  final CurrentTraceContext currentTraceContext;
  final HttpClientHandler<HttpClientRequest, HttpClientResponse> handler;

  TracingInterceptor(HttpTracing httpTracing) {
    if (httpTracing == null) throw new NullPointerException("HttpTracing == null");
    currentTraceContext = httpTracing.tracing().currentTraceContext();
    handler = HttpClientHandler.create(httpTracing);
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RequestWrapper request = new RequestWrapper(chain.request());

    Span span;
    TraceContext parent = chain.request().tag(TraceContext.class);
    if (parent != null) { // TracingCallFactory setup this request
      span = handler.handleSendWithParent(request, parent != NULL_SENTINEL ? parent : null);
    } else { // This is using interceptors only
      span = handler.handleSend(request);
    }

    parseRouteAddress(chain, span);

    Response response = null;
    Throwable error = null;
    try (Scope scope = currentTraceContext.newScope(span.context())) {
      return response = chain.proceed(request.build());
    } catch (Throwable t) {
      error = t;
      throw t;
    } finally {
      // Intentionally not the same instance as chain.proceed, as properties may have changed
      if (response != null) request = new RequestWrapper(response.request());
      handler.handleReceive(new ResponseWrapper(request, response, error), span);
    }
  }

  static void parseRouteAddress(Chain chain, Span span) {
    if (span.isNoop()) return;
    Connection connection = chain.connection();
    if (connection == null) return;
    InetSocketAddress socketAddress = connection.route().socketAddress();
    span.remoteIpAndPort(socketAddress.getHostString(), socketAddress.getPort());
  }

  static final class RequestWrapper extends HttpClientRequest {
    final Request delegate;
    Request.Builder builder;

    RequestWrapper(Request delegate) {
      this.delegate = delegate;
    }

    @Override public Object unwrap() {
      return delegate;
    }

    @Override public String method() {
      return delegate.method();
    }

    @Override public String path() {
      return delegate.url().encodedPath();
    }

    @Override public String url() {
      return delegate.url().toString();
    }

    @Override public String header(String name) {
      return delegate.header(name);
    }

    @Override public void header(String name, String value) {
      if (builder == null) builder = delegate.newBuilder();
      builder.header(name, value);
    }

    Request build() {
      return builder != null ? builder.build() : delegate;
    }
  }

  static final class ResponseWrapper extends HttpClientResponse {
    final RequestWrapper request;
    @Nullable final Response response;
    @Nullable final Throwable error;

    ResponseWrapper(RequestWrapper request, @Nullable Response response,
      @Nullable Throwable error) {
      this.request = request;
      this.response = response;
      this.error = error;
    }

    @Override public Object unwrap() {
      return response;
    }

    @Override public RequestWrapper request() {
      return request;
    }

    @Override public Throwable error() {
      return error;
    }

    @Override public int statusCode() {
      return response != null ? response.code() : 0;
    }
  }
}
