/*
 *     Licensed to the Apache Software Foundation (ASF) under one or more
 *     contributor license agreements.  See the NOTICE file distributed with
 *     this work for additional information regarding copyright ownership.
 *     The ASF licenses this file to You 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 org.apache.openejb.cdi.internal;

import org.apache.openejb.AppContext;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.assembler.classic.AppInfo;
import org.apache.openejb.cdi.OpenEJBLifecycle;
import org.apache.openejb.cdi.api.Hessian;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.server.hessian.HessianRegistry;
import org.apache.openejb.server.hessian.HessianServer;
import org.apache.openejb.server.hessian.HessianService;
import org.apache.openejb.spi.ContainerSystem;
import org.apache.openejb.util.AppFinder;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.container.BeanManagerImpl;
import org.apache.webbeans.container.InjectableBeanManager;

import javax.enterprise.context.Dependent;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.BeforeShutdown;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessBean;
import javax.enterprise.inject.spi.ProcessSessionBean;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;

public class HessianExtension implements Extension {
    private static final Logger LOGGER = Logger.getLogger(HessianExtension.class.getName());

    private final Collection<Deployment> toDeploy = new ArrayList<>();
    private final Collection<DeployedEndpoint> deployed = new ArrayList<>();

    private AppInfo appInfo;

    protected void init(final @Observes BeforeBeanDiscovery beforeBeanDiscovery) {
        appInfo = OpenEJBLifecycle.CURRENT_APP_INFO.get();
        if (appInfo == null) {
            throw new IllegalStateException("Without OpenEJBLifecycle this Extension can't work correctly");
        }
    }

    protected <X> void findHessianWebServices(final @Observes ProcessBean<X> processBean) {
        if (ProcessSessionBean.class.isInstance(processBean)) {
            return;
        }

        final Bean<X> bean = processBean.getBean();
        final Class<?> beanClass = bean.getBeanClass();
        for (final Class<?> itf : beanClass.getInterfaces()) {
            final Hessian hessian = itf.getAnnotation(Hessian.class);
            final String key = "openejb.hessian." + beanClass.getName() + "_" + itf.getName() + ".path";
            final String path = appInfo.properties.getProperty(key, SystemInstance.get().getProperty(key));
            if (hessian != null || path != null) {
                toDeploy.add(new Deployment(itf, path, bean));
            }
        }
    }

    protected void deploy(final @Observes AfterDeploymentValidation afterDeploymentValidation, final BeanManager bm) {
        final HessianService service = SystemInstance.get().getComponent(HessianService.class);
        if (service == null) {
            LOGGER.severe("HessianService not yet started, no hessian CDI webservices will be deployed");
            return;
        }

        final HessianRegistry registry = service.getRegistry();

        final String appName = findAppName();
        for (final Deployment deployment : toDeploy) {
            final Hessian hessian = deployment.itf.getAnnotation(Hessian.class);
            final HessianServer server;
            try {
                server = new HessianServer(deployment.bean.getBeanClass().getClassLoader());
            } catch (final HessianServer.HessianIsMissingException e) {
                LOGGER.info("Hessian is not available so openejb-hessian will not deploy any service");
                break;
            }

            try {
                if (hessian != null && hessian.serializerFactory() != Object.class && !hessian.serializerFactory().isInstance(server.getSerializerFactory())) {
                    server.serializerFactory(hessian.serializerFactory().newInstance());
                }
            } catch (final Exception e) {
                throw new OpenEJBRuntimeException(e);
            }
            if (hessian != null) {
                server.sendCollectionType(hessian.sendCollectionType());
            }
            if (Dependent.class.equals(deployment.bean.getScope())) {
                LOGGER.warning("@Dependent can lead to memory leaks ATM");
            }
            server.createSkeleton(bm.getReference(deployment.bean, deployment.itf, null), deployment.itf);

            final String name = getName(deployment.path, deployment.itf);
            try {
                LOGGER.info("Hessian(url=" + registry.deploy(deployment.itf.getClassLoader(), server,
                    service.getVirtualHost(), appName,
                    service.getAuthMethod(), service.getTransportGuarantee(),
                    service.getRealmName(), name) + ", interface=" + name + ")");
                deployed.add(new DeployedEndpoint(appName, name));
            } catch (final URISyntaxException e) {
                throw new OpenEJBRuntimeException(e);
            }
        }
        toDeploy.clear();
    }

    private String findAppName() {
        if (appInfo.webAppAlone) {
            return appInfo.webApps.iterator().next().contextRoot;
        }

        for (final AppContext app : SystemInstance.get().getComponent(ContainerSystem.class).getAppContexts()) {
            for (final WebContext webContext : app.getWebContexts()) {
                if (isSameContext(webContext.getWebBeansContext())) {
                    String contextRoot = webContext.getContextRoot();
                    if (contextRoot != null) {
                        if (contextRoot.startsWith("/")) {
                            return contextRoot.substring(1);
                        }
                        return contextRoot;
                    }
                    return webContext.getId();
                }
            }
            if (isSameContext(app.getWebBeansContext())) {
                return app.getId();
            }
        }
        throw new IllegalArgumentException("Can't find application matching bean manager");
    }

    private boolean isSameContext(final WebBeansContext app) {
        final BeanManagerImpl bm = app.getBeanManagerImpl();
        try {
            return bm.isInUse() && equals(bm.getExtension(HessianExtension.class));
        } catch (final Exception e) {
            return false;
        }
    }

    protected void shutdown(final @Observes BeforeShutdown unused) {
        final HessianService service = SystemInstance.get().getComponent(HessianService.class);
        if (service == null) {
            return;
        }

        final HessianRegistry registry = service.getRegistry();
        for (final DeployedEndpoint pair : deployed) {
            registry.undeploy(service.getVirtualHost(), pair.app, pair.name);
            LOGGER.info("Undeployed CDI hessian service " + pair.name);
        }
        deployed.clear();
    }

    private static String getName(final String path, final Class<?> itf) {
        if (path != null) {
            return path;
        }

        final Hessian hessian = itf.getAnnotation(Hessian.class);
        if (hessian != null) {
            final String name = hessian.path();
            if (name.isEmpty()) {
                return itf.getName();
            }
            return name;
        }
        return itf.getName();
    }

    protected static class Deployment {
        private final Class<?> itf;
        private final String path;
        private final Bean<?> bean;

        public Deployment(final Class<?> itf, final String path, final Bean<?> bean) {
            this.itf = itf;
            this.path = path;
            this.bean = bean;
        }
    }

    protected static class DeployedEndpoint {
        private final String app;
        private final String name;

        protected DeployedEndpoint(final String app, final String name) {
            this.app = app;
            this.name = name;
        }
    }
}
