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

import org.apache.shiro.config.Ini;
import org.apache.shiro.ini.IniFactorySupport;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.config.ogdl.ReflectionBuilder;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterConfig;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * A {@link Factory} that creates {@link FilterChainResolver} instances based on {@link Ini} configuration.
 *
 * @since 1.0
 */
@SuppressWarnings("deprecation")
public class IniFilterChainResolverFactory extends IniFactorySupport<FilterChainResolver> {

    /**
     * filters key.
     */
    public static final String FILTERS = "filters";
    /**
     * urls key.
     */
    public static final String URLS = "urls";

    private static final Logger LOGGER = LoggerFactory.getLogger(IniFilterChainResolverFactory.class);

    private FilterConfig filterConfig;

    private List<String> globalFilters = Collections.singletonList(DefaultFilter.invalidRequest.name());

    private boolean allowAccessByDefault;
    private boolean caseInsensitive = true;

    public IniFilterChainResolverFactory() {
        super();
    }

    public IniFilterChainResolverFactory(Ini ini) {
        super(ini);
    }

    public IniFilterChainResolverFactory(Ini ini, Map<String, ?> defaultBeans) {
        this(ini);
        this.setDefaults(defaultBeans);
    }

    public FilterConfig getFilterConfig() {
        return filterConfig;
    }

    public void setFilterConfig(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
    }

    public List<String> getGlobalFilters() {
        return globalFilters;
    }

    public void setGlobalFilters(List<String> globalFilters) {
        this.globalFilters = globalFilters;
    }

    public boolean isAllowAccessByDefault() {
        return allowAccessByDefault;
    }

    public void setAllowAccessByDefault(boolean allowAccessByDefault) {
        this.allowAccessByDefault = allowAccessByDefault;
    }

    public boolean isCaseInsensitive() {
        return caseInsensitive;
    }

    public void setCaseInsensitive(boolean caseInsensitive) {
        this.caseInsensitive = caseInsensitive;
    }

    protected FilterChainResolver createInstance(Ini ini) {
        FilterChainResolver filterChainResolver = createDefaultInstance();
        if (filterChainResolver instanceof PathMatchingFilterChainResolver resolver) {
            FilterChainManager manager = resolver.getFilterChainManager();
            buildChains(manager, ini);
        }
        return filterChainResolver;
    }

    protected FilterChainResolver createDefaultInstance() {
        FilterConfig filterConfig = getFilterConfig();
        if (filterConfig != null) {
            return new PathMatchingFilterChainResolver(filterConfig).caseInsensitive(caseInsensitive);
        } else {
            return new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
        }
    }

    protected void buildChains(FilterChainManager manager, Ini ini) {
        //filters section:
        Ini.Section section = ini.getSection(FILTERS);

        if (!CollectionUtils.isEmpty(section)) {
            String msg = "The [{}] section has been deprecated and will be removed in a future release!  Please "
                    + "move all object configuration (filters and all other objects) to the [{}] section.";
            LOGGER.warn(msg, FILTERS, IniSecurityManagerFactory.MAIN_SECTION_NAME);
        }

        Map<String, Object> defaults = new LinkedHashMap<String, Object>();

        Map<String, Filter> defaultFilters = manager.getFilters();

        //now let's see if there are any object defaults in addition to the filters
        //these can be used to configure the filters:
        //create a Map of objects to use as the defaults:
        if (!CollectionUtils.isEmpty(defaultFilters)) {
            defaults.putAll(defaultFilters);
        }
        //User-provided objects must come _after_ the default filters - to allow the user-provided
        //ones to override the default filters if necessary.
        Map<String, ?> defaultBeans = getDefaults();
        if (!CollectionUtils.isEmpty(defaultBeans)) {
            defaults.putAll(defaultBeans);
        }

        Map<String, Filter> filters = getFilters(section, defaults);

        //add the filters to the manager:
        registerFilters(filters, manager);

        manager.setGlobalFilters(getGlobalFilters());

        //urls section:
        section = ini.getSection(URLS);
        createChains(section, manager);

        // create the default chain, to match anything the path matching would have missed
        // TODO this assumes ANT path matching
        if (isAllowAccessByDefault()) {
            manager.createDefaultChain("/**", DefaultFilter.anon.name());
        } else {
            manager.createDefaultChain("/**", DefaultFilter.noAccess.name());
        }
    }

    protected void registerFilters(Map<String, Filter> filters, FilterChainManager manager) {
        if (!CollectionUtils.isEmpty(filters)) {
            //only call filter.init if there is a FilterConfig available
            boolean init = getFilterConfig() != null;
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                manager.addFilter(name, filter, init);
            }
        }
    }

    protected Map<String, Filter> getFilters(Map<String, String> section, Map<String, ?> defaults) {

        Map<String, Filter> filters = extractFilters(defaults);

        if (!CollectionUtils.isEmpty(section)) {
            ReflectionBuilder builder = new ReflectionBuilder(defaults);
            Map<String, ?> built = builder.buildObjects(section);
            Map<String, Filter> sectionFilters = extractFilters(built);

            if (CollectionUtils.isEmpty(filters)) {
                filters = sectionFilters;
            } else {
                if (!CollectionUtils.isEmpty(sectionFilters)) {
                    filters.putAll(sectionFilters);
                }
            }
        }

        return filters;
    }

    private Map<String, Filter> extractFilters(Map<String, ?> objects) {
        if (CollectionUtils.isEmpty(objects)) {
            return null;
        }
        Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>();
        for (Map.Entry<String, ?> entry : objects.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Filter filter) {
                filterMap.put(key, filter);
            }
        }
        return filterMap;
    }

    protected void createChains(Map<String, String> urls, FilterChainManager manager) {
        if (CollectionUtils.isEmpty(urls)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("No urls to process.");
            }
            return;
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Before url processing.");
        }

        for (Map.Entry<String, String> entry : urls.entrySet()) {
            String path = entry.getKey();
            String value = entry.getValue();
            manager.createChain(path, value);
        }
    }
}
