/*
 * 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.shiro.guice.web;

import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.name.Names;
import com.google.inject.servlet.ServletModule;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.env.Environment;
import org.apache.shiro.guice.ShiroModule;
import org.apache.shiro.lang.util.StringUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.web.env.WebEnvironment;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
import org.apache.shiro.web.filter.authz.IpFilter;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.filter.authz.PortFilter;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.filter.authz.SslFilter;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;

import javax.servlet.Filter;
import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@SuppressWarnings("checkstyle:JavadocVariable")
/**
 * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
 * {@link org.apache.shiro.web.mgt.WebSecurityManager},
 * {@link org.apache.shiro.mgt.SecurityManager} and {@link org.apache.shiro.session.mgt.SessionManager}.
 * At least one realm must be added by
 * using {@link #bindRealm() bindRealm}.
 * <p/>
 * Also provides for the configuring of filter chains and binds a
 * {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
 */
public abstract class ShiroWebModule extends ShiroModule {
    public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
    public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
    public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
    public static final Key<BearerHttpAuthenticationFilter> AUTHC_BEARER = Key.get(BearerHttpAuthenticationFilter.class);
    public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
    public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
    public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
    public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
    public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
    public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
    public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
    public static final Key<IpFilter> IP = Key.get(IpFilter.class);
    public static final Key<UserFilter> USER = Key.get(UserFilter.class);
    public static final Key<InvalidRequestFilter> INVALID_REQUEST = Key.get(InvalidRequestFilter.class);

    static final String NAME = "SHIRO";

    /**
     * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
     * FilterChainResolver uses iterator order when searching for a matching chain.
     */
    private final Map<String, FilterConfig<? extends Filter>[]> filterChains =
            new LinkedHashMap<String, FilterConfig<? extends Filter>[]>();
    private final ServletContext servletContext;

    public ShiroWebModule(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public static void bindGuiceFilter(Binder binder) {
        binder.install(guiceFilterModule());
    }

    public static void bindGuiceFilter(final String pattern, Binder binder) {
        binder.install(guiceFilterModule(pattern));
    }

    public static ServletModule guiceFilterModule() {
        return guiceFilterModule("/*");
    }

    public static ServletModule guiceFilterModule(final String pattern) {
        return new ServletModule() {
            @Override
            protected void configureServlets() {
                filter(pattern).through(GuiceShiroFilter.class);
            }
        };
    }

    public List<FilterConfig<? extends Filter>> globalFilters() {
        return Collections.singletonList(filterConfig(INVALID_REQUEST));
    }

    @Override
    @SuppressWarnings("unchecked")
    protected final void configureShiro() {
        bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
        bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
        bindWebSecurityManager(bind(WebSecurityManager.class));
        bindWebEnvironment(bind(WebEnvironment.class));
        bind(GuiceShiroFilter.class).asEagerSingleton();
        expose(GuiceShiroFilter.class);

        this.configureShiroWeb();

        // add default matching route if not already set
        if (!filterChains.containsKey("/**")) {
            // no config, this will add only the global filters
            this.addFilterChain("/**", new FilterConfig[0]);
        }

        bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(setupFilterChainConfigs()));
    }

    @SuppressWarnings("unchecked")
    private Map<String, Key<? extends Filter>[]> setupFilterChainConfigs() {

        // loop through and build a map of Filter Key -> Map<Path, Config>
        Map<Key<? extends Filter>, Map<String, String>> filterToPathToConfig =
                new HashMap<Key<? extends Filter>, Map<String, String>>();

        // At the same time build a map to return with Path -> Key[]
        Map<String, Key<? extends Filter>[]> resultConfigMap = new LinkedHashMap<String, Key<? extends Filter>[]>();

        for (Map.Entry<String, FilterConfig<? extends Filter>[]> filterChain : filterChains.entrySet()) {

            String path = filterChain.getKey();

            // collect the keys used for this path
            List<Key<? extends Filter>> keysForPath = new ArrayList<Key<? extends Filter>>();

            List<FilterConfig<? extends Filter>> globalFilters = this.globalFilters();
            FilterConfig<? extends Filter>[] pathFilters = filterChain.getValue();

            // merge the global filters and the path specific filters
            List<FilterConfig<? extends Filter>> filterConfigs = new ArrayList<>(globalFilters.size() + pathFilters.length);
            filterConfigs.addAll(globalFilters);
            filterConfigs.addAll(Arrays.asList(pathFilters));

            for (FilterConfig<? extends Filter> filterConfig : filterConfigs) {

                Key<? extends Filter> key = filterConfig.getKey();
                String config = filterConfig.getConfigValue();

                // initialize key in filterToPathToConfig, if it doesn't exist
                if (filterToPathToConfig.get(key) == null) {
                    // Fix for SHIRO-621: REST filter bypassing matched path
                    filterToPathToConfig.put((key), new LinkedHashMap<String, String>());
                }
                // now set the value
                filterToPathToConfig.get(key).put(path, config);

                // Config error if someone configured a non PathMatchingFilter with a config value
                if (StringUtils.hasText(config)
                        && !PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
                    throw new ConfigurationException(
                            "Config information requires a PathMatchingFilter - can't apply to "
                                    + key.getTypeLiteral().getRawType());
                }

                // store the key in keysForPath
                keysForPath.add(key);
            }

            // map the current path to all of its Keys
            resultConfigMap.put(path, keysForPath.toArray(new Key[keysForPath.size()]));
        }

        // now we find only the PathMatchingFilter and configure bindings
        // non PathMatchingFilter, can be loaded with the default provider via the class name
        for (Key<? extends Filter> key : filterToPathToConfig.keySet()) {
            if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
                bindPathMatchingFilter(castToPathMatching(key), filterToPathToConfig.get(key));
            } else {
                bind(key);
            }
        }

        return resultConfigMap;
    }


    private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
        bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
    }

    @SuppressWarnings({"unchecked"})
    private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
        return (Key<? extends PathMatchingFilter>) key;
    }

    protected abstract void configureShiroWeb();

    @SuppressWarnings({"unchecked"})
    @Override
    protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
        // SHIRO-435
        bind.to(WebSecurityManager.class);
    }

    /**
     * Binds the security manager.  Override this method in order to provide your own security manager binding.
     * <p/>
     * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
     *
     * @param bind
     */
    protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
        try {
            bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
        } catch (NoSuchMethodException e) {
            throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, "
                    + "or there's a bug in ShiroModule.", e);
        }
    }

    /**
     * Binds the session manager.  Override this method in order to provide your own session manager binding.
     * <p/>
     * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
     *
     * @param bind
     */
    @Override
    protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
        bind.to(ServletContainerSessionManager.class).asEagerSingleton();
    }

    @Override
    protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
        // SHIRO-435
        bind.to(WebEnvironment.class);
    }

    protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
        bind.to(WebGuiceEnvironment.class).asEagerSingleton();
    }

    @SuppressWarnings("unchecked")
    protected final void addFilterChain(String pattern, Key<? extends Filter> key) {
        // check for legacy API
        if (key instanceof FilterConfigKey) {
            addLegacyFilterChain(pattern, (FilterConfigKey) key);
        } else {
            addFilterChain(pattern, new FilterConfig<Filter>((Key<Filter>) key, ""));
        }
    }

    /**
     * Maps 'n' number of <code>filterConfig</code>s to a specific path pattern.<BR/>
     * For example, a path of '/my_private_resource/**' to 'filterConfig(AUTHC)' would require
     * any resource under the path '/my_private_resource' would be processed through the {@link FormAuthenticationFilter}.
     *
     * @param pattern       URL patter to be mapped to a FilterConfig, e.g. '/my_private-path/**'
     * @param filterConfigs FilterConfiguration representing the Filter
     *                      and config to be used when processing resources on <code>pattern</code>.
     * @since 1.4
     */
    @SafeVarargs
    protected final void addFilterChain(String pattern, FilterConfig<? extends Filter>... filterConfigs) {
        filterChains.put(pattern, filterConfigs);
    }

    /**
     * Builds a FilterConfig from a Filer and configuration String
     *
     * @param baseKey The Key of the Filter class to be used.
     * @param <T>     A Servlet Filter class.
     * @return A FilterConfig used to map a String path to this configuration.
     * @since 1.4
     */
    protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey, String configValue) {
        return new FilterConfig<T>(baseKey, configValue);
    }

    /**
     * Builds a FilterConfig from a Filer and configuration String
     *
     * @param baseKey The Key of the Filter class to be used.
     * @param <T>     A Servlet Filter class.
     * @return A FilterConfig used to map a String path to this configuration.
     * @since 1.4
     */
    protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey) {
        return filterConfig(baseKey, "");
    }

    /**
     * Builds a FilterConfig from a Filer and configuration String
     *
     * @param typeLiteral The TypeLiteral of the filter key to be used.
     * @param configValue the configuration used.
     * @param <T>         A Servlet Filter class.
     * @return A FilterConfig used to map a String path to this configuration.
     * @since 1.4
     */
    protected static <T extends Filter> FilterConfig<T> filterConfig(TypeLiteral<T> typeLiteral, String configValue) {
        return filterConfig(Key.get(typeLiteral), configValue);
    }

    /**
     * Builds a FilterConfig from a Filer and configuration String
     *
     * @param type        The filter to be used.
     * @param configValue the configuration used.
     * @param <T>         A Servlet Filter class.
     * @return A FilterConfig used to map a String path to this configuration.
     * @since 1.4
     */
    protected static <T extends Filter> FilterConfig<T> filterConfig(Class<T> type, String configValue) {
        return filterConfig(Key.get(type), configValue);
    }


    /**
     * Filter configuration which pairs a Filter class with its configuration used on a path.
     *
     * @param <T> The Servlet Filter class.
     * @since 1.4
     */
    public static final class FilterConfig<T extends Filter> {
        private Key<T> key;
        private String configValue;

        private FilterConfig(Key<T> key, String configValue) {
            super();
            this.key = key;
            this.configValue = configValue;
        }

        public Key<T> getKey() {
            return key;
        }

        public String getConfigValue() {
            return configValue;
        }
    }


    // legacy methods


    static boolean isGuiceVersion3() {
        try {
            Class.forName("com.google.inject.multibindings.MapKey");
            return false;
        } catch (ClassNotFoundException e) {
            return true;
        }
    }

    @SuppressWarnings("unchecked")
    private void addLegacyFilterChain(String pattern, FilterConfigKey filterConfigKey) {
        FilterConfig<Filter> filterConfig = new FilterConfig<>(filterConfigKey.getKey(), filterConfigKey.getConfigValue());
        addFilterChain(pattern, filterConfig);
    }

    /**
     * Adds a filter chain to the shiro configuration.
     * <p/>
     * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter},
     * it will be registered with a proper provider.
     *
     * @param pattern
     * @param keys
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {

        // We need to extract the keys and FilterConfigKey and convert to the new format.

        FilterConfig[] filterConfigs = new FilterConfig[keys.length];
        for (int ii = 0; ii < keys.length; ii++) {
            Key<? extends Filter> key = keys[ii];
            // If this is a path matching filter, we need to remember the config
            if (key instanceof FilterConfigKey) {
                // legacy config
                FilterConfigKey legacyKey = (FilterConfigKey) key;
                filterConfigs[ii] = new FilterConfig(legacyKey.getKey(), legacyKey.getConfigValue());
            } else {
                // Some other type of Filter key, no config
                filterConfigs[ii] = new FilterConfig(key, "");
            }
        }

        filterChains.put(pattern, filterConfigs);
    }

    @Deprecated
    protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {

        if (!isGuiceVersion3()) {
            throw new ConfigurationException(
                    "Method ShiroWebModule.config(Key<? extends PathMatchingFilter>,"
                    + " String configValue), is not supported when using Guice 4+");
        }

        return new FilterConfigKey<T>(baseKey, configValue);
    }

    @Deprecated
    protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
        return config(Key.get(typeLiteral), configValue);
    }

    @Deprecated
    protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
        return config(Key.get(type), configValue);
    }

    @Deprecated
    private static final class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
        private Key<T> key;
        private String configValue;

        private FilterConfigKey(Key<T> key, String configValue) {
            super();
            this.key = key;
            this.configValue = configValue;
        }

        public Key<T> getKey() {
            return key;
        }

        public String getConfigValue() {
            return configValue;
        }
    }

}
