/*
 * 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.nifi.controller.state.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.nifi.components.state.Scope;
import org.apache.nifi.controller.state.ConfigParseException;
import org.apache.nifi.util.DomUtils;
import org.apache.nifi.xml.processing.ProcessingException;
import org.apache.nifi.xml.processing.parsers.DocumentProvider;
import org.apache.nifi.xml.processing.parsers.StandardDocumentProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class StateManagerConfiguration {
    private final Map<String, StateProviderConfiguration> providers;

    private StateManagerConfiguration(final Map<String, StateProviderConfiguration> providerConfigs) {
        this.providers = providerConfigs;
    }

    public Map<String, StateProviderConfiguration> getStateProviderConfigurations() {
        return Collections.unmodifiableMap(providers);
    }

    public StateProviderConfiguration getStateProviderConfiguration(final String providerId) {
        return providers.get(providerId);
    }

    public List<StateProviderConfiguration> getStateProviderConfigurations(final Scope scope) {
        final List<StateProviderConfiguration> configs = new ArrayList<>();
        for (final StateProviderConfiguration config : providers.values()) {
            if (config.getScope() == scope) {
                configs.add(config);
            }
        }

        return configs;
    }

    public static StateManagerConfiguration parse(final File configFile) throws IOException, ConfigParseException {
        final Document document;
        try (final InputStream inputStream = new FileInputStream(configFile)) {
            final DocumentProvider documentProvider = new StandardDocumentProvider();
            document = documentProvider.parse(inputStream);
        } catch (final ProcessingException e) {
            throw new ConfigParseException("Unable to parse file " + configFile + ", as it does not appear to be a valid XML File", e);
        }

        final Element rootElement = document.getDocumentElement();
        final List<Element> localProviderElements = DomUtils.getChildElementsByTagName(rootElement, "local-provider");
        if (localProviderElements.isEmpty()) {
            throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                + "as it does not contain a 'local-provider' element, or the local-provider element is not the child of the root element");
        }

        final Map<String, StateProviderConfiguration> configs = new HashMap<>();
        for (final Element localProviderElement : localProviderElements) {
            final StateProviderConfiguration providerConfig = parseProviderConfiguration(localProviderElement, Scope.LOCAL, configFile);
            if (configs.containsKey(providerConfig.getId())) {
                throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                    + "as it contains multiple providers with the \"id\" of \"" + providerConfig.getId() + "\"");
            }

            configs.put(providerConfig.getId(), providerConfig);
        }

        final List<Element> clusterProviderElements = DomUtils.getChildElementsByTagName(rootElement, "cluster-provider");
        for (final Element clusterProviderElement : clusterProviderElements) {
            final StateProviderConfiguration providerConfig = parseProviderConfiguration(clusterProviderElement, Scope.CLUSTER, configFile);
            if (configs.containsKey(providerConfig.getId())) {
                throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                    + "as it contains multiple providers with the \"id\" of \"" + providerConfig.getId() + "\"");
            }

            configs.put(providerConfig.getId(), providerConfig);
        }

        return new StateManagerConfiguration(configs);
    }

    private static StateProviderConfiguration parseProviderConfiguration(final Element providerElement, final Scope scope, final File configFile) throws ConfigParseException {
        final String elementName = providerElement.getNodeName();

        final String id = DomUtils.getChildText(providerElement, "id");
        if (id == null) {
            throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                + "as a " + elementName + " element does not contain an \"id\" element");
        }
        if (id.trim().isEmpty()) {
            throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                + "as a " + elementName + "'s \"id\" element is empty");
        }

        final String className = DomUtils.getChildText(providerElement, "class");
        if (className == null) {
            throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                + "as a " + elementName + " element does not contain an \"class\" element");
        }
        if (className.trim().isEmpty()) {
            throw new ConfigParseException("State Management config file " + configFile + " is not a valid configuration file, "
                + "as a " + elementName + "'s \"class\" element is empty");
        }

        final List<Element> propertyElements = DomUtils.getChildElementsByTagName(providerElement, "property");
        final Map<String, String> propertyMap = new HashMap<>();
        for (final Element propertyElement : propertyElements) {
            final String propertyName = propertyElement.getAttribute("name");
            final String propertyValue = propertyElement.getTextContent();
            propertyMap.put(propertyName, propertyValue);
        }

        return new StateProviderConfiguration(id, className, scope, propertyMap);
    }
}
