package ru.yandex.qe.bus.features.version;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cxf.Bus;
import org.apache.cxf.feature.AbstractFeature;
import org.apache.cxf.interceptor.InterceptorProvider;
import org.apache.cxf.jaxrs.AbstractJAXRSFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.ResourceProvider;
import org.apache.cxf.message.Message;
import org.springframework.aop.support.AopUtils;

import ru.yandex.qe.spring.ApplicationVersionHolder;

/**
 * User: terry
 * Date: 05.10.13
 * Time: 22:29
 */
public class VersionFeature extends AbstractFeature {

    private static final String SERVER_VERSION_ID_HEADER_KEY = "X-qe-server-version";

    private static final String SERVER_NAME_ID_HEADER_KEY = "X-qe-server-name";

    private static final String API_NAME_ID_HEADER_KEY = "X-qe-api-name";

    private static final String API_VERSION_ID_HEADER_KEY = "X-qe-api-version";

    private static final String BUS_VERSION_ID_HEADER_KEY = "X-qe-bus-version";

    private static final String UNDEFINED = "UNDEFINED";

    private static final PackageInfo UNDEFINED_PACKAGE = new PackageInfo(UNDEFINED, UNDEFINED, UNDEFINED);

    @Override
    protected void initializeProvider(InterceptorProvider provider, Bus bus) {
        final InVersionInterceptor inInterceptor = new InVersionInterceptor(this);
        provider.getInInterceptors().add(inInterceptor);
        provider.getInFaultInterceptors().add(inInterceptor);

        provider.getOutInterceptors().add(new OutVersionInterceptor(this));
        provider.getOutFaultInterceptors().add(new OutVersionInterceptor(this));
    }

    public static void configureVersion(AbstractJAXRSFactoryBean abstractJAXRSFactoryBean, String serverName, String serverVersion, String apiName, String apiVersion) {
        final Map<String, Object> props = new HashMap<>();
        if (serverName != null) {
            props.put(SERVER_NAME_ID_HEADER_KEY, serverName);
        }
        if (serverVersion != null) {
            props.put(SERVER_VERSION_ID_HEADER_KEY, serverVersion);
        }
        if (apiName != null) {
            props.put(API_NAME_ID_HEADER_KEY, apiName);
        }
        if (apiVersion != null) {
            props.put(API_VERSION_ID_HEADER_KEY, apiVersion);
        }
        props.put(BUS_VERSION_ID_HEADER_KEY, findBusVersion());
        abstractJAXRSFactoryBean.setProperties(props);
    }

    public static PackageInfo findServerVersion(List<Object> services, List<ResourceProvider> resourceProviders) {
        final String version = ApplicationVersionHolder.getVersion();
        final String groupId = ApplicationVersionHolder.getGroupId();
        final String artifactId = ApplicationVersionHolder.getArtifactId();
        if (version != null) {
            return new PackageInfo(artifactId != null ? artifactId : UNDEFINED,
                    groupId != null ? groupId : UNDEFINED,
                    version);
        } else {
            // fallback for applications, which do not use QeWebApplication
            return tryFindVersion(services, resourceProviders, new VersionFinder() {
                @Override
                public PackageInfo find(Object resource) {
                    return findVersion(resource);
                }
            });
        }
    }

    public static PackageInfo findApiVersion(List<Object> services, List<ResourceProvider> resourceProviders) {
        return tryFindVersion(services, resourceProviders, new VersionFinder() {
            @Override
            public PackageInfo find(Object resource) {
                return findInterfaceVersion(resource);
            }
        });
    }

    public static String findBusVersion() {
        String result = VersionFeature.class.getPackage().getImplementationVersion();
        return result == null ? UNDEFINED : result;
    }

    public static PackageInfo findInterfaceVersion(Object resource) {
        if (AopUtils.isJdkDynamicProxy(resource) || AopUtils.isCglibProxy(resource)) {
            return findInterfaceVersionForClass(AopUtils.getTargetClass(resource));
        } else {
            return findInterfaceVersionForClass(resource.getClass());
        }
    }

    public static PackageInfo findInterfaceVersionForClass(Class<?> clazz) {
        if (clazz.isInterface()) {
            return findVersion(clazz);
        } else {
            for (Class<?> interfaceClass : clazz.getInterfaces()) {
                final PackageInfo info = findVersion(interfaceClass);
                if (!UNDEFINED.equals(info.getVersion())) {
                    return info;
                }
            }
        }
        return UNDEFINED_PACKAGE;
    }

    private static PackageInfo tryFindVersion(List<Object> services, List<ResourceProvider> resourceProviders, VersionFinder versionFinder) {
        for (Object service : services) {
            final PackageInfo version = versionFinder.find(service);
            if (!UNDEFINED.equals(version.getVersion())) {
                return version;
            }
        }
        for (ResourceProvider resourceProvider : resourceProviders) {
            final PackageInfo version = versionFinder.find(resourceProvider);
            if (!UNDEFINED.equals(version.getVersion())) {
                return version;
            }
        }
        return UNDEFINED_PACKAGE;
    }

    private static PackageInfo findVersion(Object resource) {
        return findVersion(AopUtils.getTargetClass(resource));
    }

    private static PackageInfo findVersion(Class<?> clazz) {
        final Package thePackage = clazz.getPackage();
        final String implementationVersion = thePackage.getImplementationVersion();
        final String version = implementationVersion != null ? implementationVersion : UNDEFINED;
        final String implementationTitle = thePackage.getImplementationTitle();
        final String title = implementationTitle != null ? implementationTitle : UNDEFINED;
        final String implementationVendor = thePackage.getImplementationVendor();
        final String vendor = implementationVendor != null ? implementationVendor : UNDEFINED;
        return new PackageInfo(title, vendor, version);
    }

    @SuppressWarnings("unchecked")
    private void putVersionInHeaders(Message message, String versionKey) {
        final Object versionAttribute = message.getExchange().getEndpoint().get(versionKey);
        if (versionAttribute != null) {
            Map<String, List<String>> headers = (Map<String, List<String>>) message.get(Message.PROTOCOL_HEADERS);
            if (headers == null) {
                headers = new HashMap<>();
                message.put(Message.PROTOCOL_HEADERS, headers);
            }
            headers.put(versionKey, Collections.<String>singletonList(versionAttribute.toString()));
        }
    }

    protected void putVersionsInHeaders(Message message) {
        putVersionInHeaders(message, SERVER_NAME_ID_HEADER_KEY);
        putVersionInHeaders(message, SERVER_VERSION_ID_HEADER_KEY);
        putVersionInHeaders(message, API_NAME_ID_HEADER_KEY);
        putVersionInHeaders(message, API_VERSION_ID_HEADER_KEY);
        putVersionInHeaders(message, BUS_VERSION_ID_HEADER_KEY);
    }

    private static interface VersionFinder {
        PackageInfo find(Object resource);
    }

    public static class PackageInfo {
        private final String title;
        private final String vendor;
        private final String version;

        private PackageInfo(final String title, final String vendor, final String version) {
            this.title = title;
            this.vendor = vendor;
            this.version = version;
        }

        public String getTitle() {
            return title;
        }

        public String getVendor() {
            return vendor;
        }

        public String getVersion() {
            return version;
        }
    }
}
