/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.instrumentation.apachehttpclient.v5_2;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount;
import java.io.IOException;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChain.Scope;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;

class OtelExecChainHandler implements ExecChainHandler {

  private static final String REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID =
      OtelExecChainHandler.class.getName() + ".context";

  private final Instrumenter<ApacheHttpClient5Request, HttpResponse> instrumenter;
  private final ContextPropagators propagators;

  public OtelExecChainHandler(
      Instrumenter<ApacheHttpClient5Request, HttpResponse> instrumenter,
      ContextPropagators propagators) {
    this.instrumenter = instrumenter;
    this.propagators = propagators;
  }

  @Override
  public ClassicHttpResponse execute(ClassicHttpRequest request, Scope scope, ExecChain chain)
      throws IOException, HttpException {
    Context parentContext =
        scope.clientContext.getAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, Context.class);
    request.setVersion(scope.clientContext.getProtocolVersion());
    if (parentContext == null) {
      parentContext = HttpClientRequestResendCount.initialize(Context.current());
      scope.clientContext.setAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, parentContext);
    }

    ApacheHttpClient5Request instrumenterRequest = getApacheHttpClient5Request(request, scope);

    if (!instrumenter.shouldStart(parentContext, instrumenterRequest)) {
      return chain.proceed(request, scope);
    }

    Context context = instrumenter.start(parentContext, instrumenterRequest);
    propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE);

    return execute(request, instrumenterRequest, chain, scope, context);
  }

  private ClassicHttpResponse execute(
      ClassicHttpRequest request,
      ApacheHttpClient5Request instrumenterRequest,
      ExecChain chain,
      Scope scope,
      Context context)
      throws IOException, HttpException {
    ClassicHttpResponse response = null;
    Throwable error = null;
    try (io.opentelemetry.context.Scope ignored = context.makeCurrent()) {
      response = chain.proceed(request, scope);
      return response;
    } catch (Exception e) {
      error = e;
      throw e;
    } finally {
      instrumenter.end(context, instrumenterRequest, response, error);
    }
  }

  private static ApacheHttpClient5Request getApacheHttpClient5Request(
      ClassicHttpRequest request, Scope scope) {
    HttpHost host = null;
    if (scope.route.getTargetHost() != null) {
      host = scope.route.getTargetHost();
    } else if (scope.clientContext.getHttpRoute().getTargetHost() != null) {
      host = scope.clientContext.getHttpRoute().getTargetHost();
    }
    if (host != null
        && ((host.getSchemeName().equals("https") && host.getPort() == 443)
            || (host.getSchemeName().equals("http") && host.getPort() == 80))) {
      // port seems to be added to the host by route planning for standard ports even if not
      // specified in the URL. There doesn't seem to be a way to differentiate between explicit
      // and implicit port, but ignore in both cases to match the more common case.
      host = new HttpHost(host.getSchemeName(), host.getHostName(), -1);
    }

    return new ApacheHttpClient5Request(host, request);
  }
}
