/*
 * 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.unomi.scripting.internal;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.unomi.scripting.ExpressionFilter;
import org.apache.unomi.scripting.ExpressionFilterFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;

public class ExpressionFilterFactoryImpl implements ExpressionFilterFactory,BundleListener {

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

    private final Map<Bundle,Map<String,Set<Pattern>>> allowedExpressionPatternsByBundle = new HashMap<>();

    private final Map<String,Set<Pattern>> allowedExpressionPatternsByCollection = new HashMap<>();
    private final Map<String,Set<Pattern>> forbiddenExpressionPatternsByCollection = new HashMap<>();

    private BundleContext bundleContext = null;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private boolean expressionFiltersActivated = Boolean.parseBoolean(System.getProperty("org.apache.unomi.scripting.filter.activated", "true"));

    public void setBundleContext(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
    }

    public ExpressionFilterFactoryImpl() {
    }

    public void init() {
        String initialFilterCollections = System.getProperty("org.apache.unomi.scripting.filter.collections", "mvel,ognl");
        String[] initialFilterCollectionParts = initialFilterCollections.split(",");
        if (initialFilterCollectionParts != null) {
            for (String initialFilterCollection : initialFilterCollectionParts) {
                allowedExpressionPatternsByCollection.put(initialFilterCollection, loadPatternsFromConfig("org.apache.unomi.scripting.filter."+initialFilterCollection+".allow"));
                forbiddenExpressionPatternsByCollection.put(initialFilterCollection, loadPatternsFromConfig("org.apache.unomi.scripting.filter."+initialFilterCollection+".forbid"));
            }
        }

        if (bundleContext != null) {
            loadPredefinedAllowedPatterns(bundleContext);
            for (Bundle bundle : bundleContext.getBundles()) {
                if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
                    loadPredefinedAllowedPatterns(bundle.getBundleContext());
                }
            }

            bundleContext.addBundleListener(this);
        }
    }

    private Set<Pattern> loadPatternsFromConfig(String propertyKey) {
        String patternsFile = System.getProperty(propertyKey, null);
        if (StringUtils.isNotEmpty(patternsFile)) {
            Set<Pattern> patterns = new HashSet<>();
            try {
                JsonNode jsonPatterns = objectMapper.readTree(new File(patternsFile));
                for (JsonNode jsonPattern : jsonPatterns) {
                    patterns.add(Pattern.compile(jsonPattern.asText()));
                }
            } catch (IOException e) {
                LOGGER.error("Error while loading expressions definition from {}", propertyKey, e);
            }
            return patterns;
        }
        return null;
    }

    public void destroy() {
        if (bundleContext != null) {
            bundleContext.removeBundleListener(this);
        }
    }

    public void bundleChanged(BundleEvent event) {
        switch (event.getType()) {
            case BundleEvent.STARTED:
                processBundleStartup(event.getBundle().getBundleContext());
                break;
            case BundleEvent.STOPPING:
                processBundleStop(event.getBundle().getBundleContext());
                break;
        }
    }

    private void processBundleStartup(BundleContext bundleContext) {
        if (bundleContext == null) {
            return;
        }
        loadPredefinedAllowedPatterns(bundleContext);
    }

    private void processBundleStop(BundleContext bundleContext) {
        if (bundleContext == null) {
            return;
        }
        removePredefinedAllowedPatterns(bundleContext);
    }

    private void loadPredefinedAllowedPatterns(BundleContext bundleContext) {
        Enumeration<URL> predefinedAllowedExpressions = bundleContext.getBundle().findEntries("META-INF/cxs/expressions", "*.json", true);
        if (predefinedAllowedExpressions == null) {
            return;
        }

        Map<String,Set<Pattern>> predefinedAllowedExpressionsForBundle = new HashMap<>();

        while (predefinedAllowedExpressions.hasMoreElements()) {
            URL predefinedAllowedExpressionsURL = predefinedAllowedExpressions.nextElement();
            LOGGER.debug("Found predefined allowed expressions at {}, loading... ", predefinedAllowedExpressionsURL);
            try {
                JsonNode predefinedAllowedExpressionsNode = objectMapper.readTree(predefinedAllowedExpressionsURL);
                Set<Pattern> bundleAllowedExpressions = new HashSet<>();
                for (JsonNode predefinedAllowedExpressionNode : predefinedAllowedExpressionsNode) {
                    bundleAllowedExpressions.add(Pattern.compile(predefinedAllowedExpressionNode.asText()));
                }
                String collection = predefinedAllowedExpressionsURL.getFile().substring("/META-INF/cxs/expressions/".length(), predefinedAllowedExpressionsURL.getFile().length() - ".json".length());
                predefinedAllowedExpressionsForBundle.put(collection, bundleAllowedExpressions);
                Set<Pattern> existingAllowedExpressions = allowedExpressionPatternsByCollection.get(collection);
                if (existingAllowedExpressions == null) {
                    existingAllowedExpressions = new HashSet<>();
                }
                existingAllowedExpressions.addAll(bundleAllowedExpressions);
                allowedExpressionPatternsByCollection.put(collection, existingAllowedExpressions);
            } catch (IOException e) {
                LOGGER.error("Error while loading expressions definition {}", predefinedAllowedExpressionsURL, e);
            }
        }

        allowedExpressionPatternsByBundle.put(bundleContext.getBundle(), predefinedAllowedExpressionsForBundle);
    }

    private void removePredefinedAllowedPatterns(BundleContext bundleContext) {
        Map<String,Set<Pattern>> allowedExpressionPatternsForBundle = allowedExpressionPatternsByBundle.get(bundleContext.getBundle());
        for (Map.Entry<String,Set<Pattern>> allowedExpressionPatternsEntry : allowedExpressionPatternsForBundle.entrySet()) {
            Set<Pattern> allowedExpressionPatterns = allowedExpressionPatternsByCollection.get(allowedExpressionPatternsEntry.getKey());
            allowedExpressionPatterns.removeAll(allowedExpressionPatternsEntry.getValue());
            allowedExpressionPatternsByCollection.put(allowedExpressionPatternsEntry.getKey(), allowedExpressionPatterns);
        }
    }

    @Override
    public ExpressionFilter getExpressionFilter(String filterCollection) {
        if (expressionFiltersActivated) {
            return new ExpressionFilter(allowedExpressionPatternsByCollection.get(filterCollection), forbiddenExpressionPatternsByCollection.get(filterCollection));
        } else {
            // if expression filtering is turned off we build an expression filter with no filters and that will accept everything.
            return new ExpressionFilter(null, null);
        }
    }
}
