/*
 * Copyright 2013-2021 the original author or 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
 *
 *      https://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 org.springframework.cloud.sleuth.instrument.web.client.feign;

import java.lang.reflect.Field;

import feign.Client;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
import org.springframework.cloud.util.ProxyUtils;
import org.springframework.util.ClassUtils;

/**
 * Class that wraps Feign related classes into their Trace representative.
 *
 * @author Marcin Grzejszczak
 * @author Olga Maciaszek-Sharma
 * @since 1.0.1
 */
final class TraceFeignObjectWrapper {

	public static final String EXCEPTION_WARNING = "Exception occurred while trying to access the delegate's field. Will fallback to default instrumentation mechanism, which means that the delegate might not be instrumented";

	private static final Log log = LogFactory.getLog(TraceFeignObjectWrapper.class);

	private static final boolean loadBalancerPresent;

	private static final String DELEGATE = "delegate";

	static {
		loadBalancerPresent = ClassUtils
				.isPresent("org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient", null)
				&& ClassUtils.isPresent(
						"org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient", null)
				&& ClassUtils.isPresent("org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory",
						null);
	}

	private final BeanFactory beanFactory;

	private Object loadBalancerClient;

	private LoadBalancerClientsProperties loadBalancerProperties;

	private Object loadBalancerRetryFactory;

	private Object loadBalancerClientFactory;

	private final TraceFeignBuilderBeanPostProcessor traceFeignBuilderBeanPostProcessor;

	TraceFeignObjectWrapper(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
		this.traceFeignBuilderBeanPostProcessor = new TraceFeignBuilderBeanPostProcessor(beanFactory);
	}

	Object wrap(Object bean) {
		if (bean instanceof Client && !(bean instanceof TracingFeignClient)
				&& !(bean instanceof LazyTracingFeignClient)) {
			if (loadBalancerPresent && bean instanceof FeignBlockingLoadBalancerClient
					&& !(bean instanceof TraceFeignBlockingLoadBalancerClient)) {
				return instrumentedFeignLoadBalancerClient(bean);
			}
			if (loadBalancerPresent && bean instanceof RetryableFeignBlockingLoadBalancerClient
					&& !(bean instanceof TraceRetryableFeignBlockingLoadBalancerClient)) {
				return instrumentedRetryableFeignLoadBalancerClient(bean);
			}
			return new LazyTracingFeignClient(this.beanFactory, (Client) bean);
		}
		return this.traceFeignBuilderBeanPostProcessor.postProcessAfterInitialization(bean, null);
	}

	private Object instrumentedFeignLoadBalancerClient(Object bean) {
		if (AopUtils.getTargetClass(bean).equals(FeignBlockingLoadBalancerClient.class)) {
			FeignBlockingLoadBalancerClient client = ProxyUtils.getTargetObject(bean);
			return new TraceFeignBlockingLoadBalancerClient(
					(Client) new TraceFeignObjectWrapper(this.beanFactory).wrap(client.getDelegate()),
					(LoadBalancerClient) loadBalancerClient(), loadBalancerClientsProperties(),
					(LoadBalancerClientFactory) loadBalancerClientFactory(), this.beanFactory);
		}
		else {
			FeignBlockingLoadBalancerClient client = ProxyUtils.getTargetObject(bean);
			try {
				Field delegate = FeignBlockingLoadBalancerClient.class.getDeclaredField(DELEGATE);
				delegate.setAccessible(true);
				delegate.set(client, new TraceFeignObjectWrapper(this.beanFactory).wrap(client.getDelegate()));
			}
			catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | SecurityException e) {
				log.warn(EXCEPTION_WARNING, e);
			}
			return new TraceFeignBlockingLoadBalancerClient(client, (LoadBalancerClient) loadBalancerClient(),
					loadBalancerClientsProperties(), (LoadBalancerClientFactory) loadBalancerClientFactory(),
					this.beanFactory);
		}
	}

	private Object instrumentedRetryableFeignLoadBalancerClient(Object bean) {
		if (AopUtils.getTargetClass(bean).equals(RetryableFeignBlockingLoadBalancerClient.class)) {
			RetryableFeignBlockingLoadBalancerClient client = ProxyUtils.getTargetObject(bean);
			return new TraceRetryableFeignBlockingLoadBalancerClient(
					(Client) new TraceFeignObjectWrapper(beanFactory).wrap(client.getDelegate()),
					(BlockingLoadBalancerClient) loadBalancerClient(),
					(LoadBalancedRetryFactory) loadBalancerRetryFactory(), loadBalancerClientsProperties(),
					(LoadBalancerClientFactory) loadBalancerClientFactory(), beanFactory);
		}
		else {
			RetryableFeignBlockingLoadBalancerClient client = ((RetryableFeignBlockingLoadBalancerClient) bean);
			try {
				Field delegate = RetryableFeignBlockingLoadBalancerClient.class.getDeclaredField(DELEGATE);
				delegate.setAccessible(true);
				delegate.set(client, new TraceFeignObjectWrapper(beanFactory).wrap(client.getDelegate()));
			}
			catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | SecurityException e) {
				log.warn(EXCEPTION_WARNING, e);
			}
			return new TraceRetryableFeignBlockingLoadBalancerClient(client,
					(BlockingLoadBalancerClient) loadBalancerClient(),
					(LoadBalancedRetryFactory) loadBalancerRetryFactory(), loadBalancerClientsProperties(),
					(LoadBalancerClientFactory) loadBalancerClientFactory(), beanFactory);
		}
	}

	private Object loadBalancerClient() {
		if (loadBalancerClient == null) {
			loadBalancerClient = beanFactory.getBean(LoadBalancerClient.class);
		}
		return loadBalancerClient;
	}

	private LoadBalancerClientsProperties loadBalancerClientsProperties() {
		if (loadBalancerProperties == null) {
			loadBalancerProperties = beanFactory.getBean(LoadBalancerClientsProperties.class);
		}
		return loadBalancerProperties;
	}

	private Object loadBalancerRetryFactory() {
		if (loadBalancerRetryFactory == null) {
			loadBalancerRetryFactory = beanFactory.getBean(LoadBalancedRetryFactory.class);
		}
		return loadBalancerRetryFactory;
	}

	private Object loadBalancerClientFactory() {
		if (loadBalancerClientFactory == null) {
			loadBalancerClientFactory = beanFactory.getBean(LoadBalancerClientFactory.class);
		}
		return loadBalancerClientFactory;
	}

}
