/*
 * 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.server.hessian;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Service;
import org.apache.catalina.Valve;
import org.apache.catalina.Wrapper;
import org.apache.catalina.authenticator.BasicAuthenticator;
import org.apache.catalina.authenticator.DigestAuthenticator;
import org.apache.catalina.authenticator.NonLoginAuthenticator;
import org.apache.catalina.authenticator.SSLAuthenticator;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardServer;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.openejb.assembler.classic.WebAppBuilder;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.server.httpd.util.HttpUtil;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomee.catalina.IgnoredStandardContext;
import org.apache.tomee.catalina.OpenEJBValve;
import org.apache.tomee.catalina.TomcatWebAppBuilder;
import org.apache.tomee.loader.TomcatHelper;

import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TomcatHessianRegistry implements HessianRegistry {
    private static final String TOMEE_HESSIAN_SECURITY_ROLE_PREFIX = "tomee.hessian.security-role.";

    private final Map<String, Pair<Context, Integer>> fakeContexts = new HashMap<>();

    private Engine engine;
    private List<Connector> connectors;

    public TomcatHessianRegistry() {
        final StandardServer standardServer = TomcatHelper.getServer();
        for (final Service service : standardServer.findServices()) {
            if (Engine.class.isInstance(service.getContainer())) {
                connectors = Arrays.asList(service.findConnectors());
                engine = Engine.class.cast(service.getContainer());
                break;
            }
        }
    }

    @Override
    public String deploy(final ClassLoader loader, final HessianServer listener,
                         final String hostname, final String app,
                         final String authMethod, final String transportGuarantee,
                         final String realmName, final String name) throws URISyntaxException {
        Container host = engine.findChild(hostname);
        if (host == null) {
            host = engine.findChild(engine.getDefaultHost());
            if (host == null) {
                throw new IllegalArgumentException("Invalid virtual host '" + engine.getDefaultHost() + "'.  Do you have a matchiing Host entry in the server.xml?");
            }
        }

        final String contextRoot = contextName(app);
        Context context = Context.class.cast(host.findChild(contextRoot));
        if (context == null) {
            Pair<Context, Integer> fakeContext = fakeContexts.get(contextRoot);
            if (fakeContext != null) {
                context = fakeContext.getLeft();
                fakeContext.setValue(fakeContext.getValue() + 1);
            } else {
                context = Context.class.cast(host.findChild(contextRoot));
                if (context == null) {
                    fakeContext = fakeContexts.get(contextRoot);
                    if (fakeContext == null) {
                        context = createNewContext(loader, authMethod, transportGuarantee, realmName, app);
                        fakeContext = new MutablePair<>(context, 1);
                        fakeContexts.put(contextRoot, fakeContext);
                    } else {
                        context = fakeContext.getLeft();
                        fakeContext.setValue(fakeContext.getValue() + 1);
                    }
                }
            }
        }

        final String servletMapping = generateServletPath(name);

        Wrapper wrapper = Wrapper.class.cast(context.findChild(servletMapping));
        if (wrapper != null) {
            throw new IllegalArgumentException("Servlet " + servletMapping + " in web application context " + context.getName() + " already exists");
        }

        wrapper = context.createWrapper();
        wrapper.setName(HESSIAN.replace("/", "") + "_" + name);
        wrapper.setServlet(new OpenEJBHessianServlet(listener));
        context.addChild(wrapper);
        context.addServletMapping(servletMapping, wrapper.getName());

        if ("BASIC".equals(authMethod) && StandardContext.class.isInstance(context)) {
            final StandardContext standardContext = StandardContext.class.cast(context);

            boolean found = false;
            for (final Valve v : standardContext.getPipeline().getValves()) {
                if (LimitedBasicValve.class.isInstance(v) || BasicAuthenticator.class.isInstance(v)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                standardContext.addValve(new LimitedBasicValve());
            }
        }

        final List<String> addresses = new ArrayList<>();
        for (final Connector connector : connectors) {
            for (final String mapping : wrapper.findMappings()) {
                final URI address = new URI(connector.getScheme(), null, host.getName(), connector.getPort(), contextRoot + mapping, null, null);
                addresses.add(address.toString());
            }
        }
        return HttpUtil.selectSingleAddress(addresses);
    }

    private static Context createNewContext(final ClassLoader classLoader, final String rAuthMethod, final String rTransportGuarantee, final String realmName, final String name) {
        String path = name;
        if (path == null) {
            path = "/";
        }
        if (!path.startsWith("/")) {
            path = "/" + path;
        }

        final StandardContext context = new IgnoredStandardContext();
        context.setPath(path);
        context.setDocBase("");
        context.setParentClassLoader(classLoader);
        context.setDelegate(true);
        context.setName(name);
        TomcatWebAppBuilder.class.cast(SystemInstance.get().getComponent(WebAppBuilder.class)).initJ2EEInfo(context);

        // Configure security
        String authMethod = rAuthMethod;
        if (authMethod != null) {
            authMethod = authMethod.toUpperCase();
        }
        String transportGuarantee = rTransportGuarantee;
        if (transportGuarantee != null) {
            transportGuarantee = transportGuarantee.toUpperCase();
        }
        if (authMethod != null & !"NONE".equals(authMethod)) {
            if ("BASIC".equals(authMethod) || "DIGEST".equals(authMethod) || "CLIENT-CERT".equals(authMethod)) {

                //Setup a login configuration
                final LoginConfig loginConfig = new LoginConfig();
                loginConfig.setAuthMethod(authMethod);
                loginConfig.setRealmName(realmName);
                context.setLoginConfig(loginConfig);

                //Setup a default Security Constraint
                final String securityRole = SystemInstance.get().getProperty(TOMEE_HESSIAN_SECURITY_ROLE_PREFIX + name, "default");
                for (final String role : securityRole.split(",")) {
                    final SecurityCollection collection = new SecurityCollection();
                    collection.addMethod("GET");
                    collection.addMethod("POST");
                    collection.addPattern("/*");
                    collection.setName(role);

                    final SecurityConstraint sc = new SecurityConstraint();
                    sc.addAuthRole("*");
                    sc.addCollection(collection);
                    sc.setAuthConstraint(true);
                    sc.setUserConstraint(transportGuarantee);

                    context.addConstraint(sc);
                    context.addSecurityRole(role);
                }
            }

            //Set the proper authenticator
            switch (authMethod) {
                case "BASIC":
                    context.addValve(new BasicAuthenticator());
                    break;
                case "DIGEST":
                    context.addValve(new DigestAuthenticator());
                    break;
                case "CLIENT-CERT":
                    context.addValve(new SSLAuthenticator());
                    break;
                case "NONE":
                    context.addValve(new NonLoginAuthenticator());
                    break;
            }

            context.getPipeline().addValve(new OpenEJBValve());
        } else {
            throw new IllegalArgumentException("Invalid authMethod: " + authMethod);
        }

        return context;
    }

    private static String generateServletPath(String name) {
        return HESSIAN + name;
    }

    private static String contextName(final String app) {
        if (!app.startsWith("/") && !app.isEmpty()) {
            return "/" + app;
        }
        return app;
    }

    @Override
    public void undeploy(final String hostname, final String app, final String name) {
        Container host = engine.findChild(hostname);
        if (host == null) {
            host = engine.findChild(engine.getDefaultHost());
            if (host == null) {
                throw new IllegalArgumentException("Invalid virtual host '" + engine.getDefaultHost() + "'.  Do you have a matchiing Host entry in the server.xml?");
            }
        }

        final String contextRoot = contextName(app);
        final Pair<Context, Integer> fakeContext = fakeContexts.get(contextRoot);

        if (fakeContext != null) {
            fakeContext.setValue(fakeContext.getValue() - 1);
            if (fakeContext.getValue() == 0) {
                fakeContexts.remove(contextRoot);
                host.removeChild(fakeContext.getKey());
            }
        }
    }

    protected static class LimitedBasicValve extends BasicAuthenticator {
        @Override
        public void invoke(final Request request, final Response response) throws IOException, ServletException {
            final String requestURI = request.getDecodedRequestURI();
            if (requestURI.startsWith(HESSIAN)) {
                if (!authenticate(request, response)) {
                    return;
                }
            }
            getNext().invoke(request, response);
        }
    }
}
