/**
 * 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.camel.runtimecatalog;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.apache.camel.runtimecatalog.URISupport.isEmpty;

/**
 * Details result of validating endpoint uri.
 */
public class EndpointValidationResult implements Serializable {

    private final String uri;
    private int errors;

    // general
    private String syntaxError;
    private String unknownComponent;
    private String incapable;

    // options
    private Set<String> unknown;
    private Map<String, String[]> unknownSuggestions;
    private Set<String> lenient;
    private Set<String> notConsumerOnly;
    private Set<String> notProducerOnly;
    private Set<String> required;
    private Map<String, String> invalidEnum;
    private Map<String, String[]> invalidEnumChoices;
    private Map<String, String[]> invalidEnumSuggestions;
    private Map<String, String> invalidReference;
    private Map<String, String> invalidBoolean;
    private Map<String, String> invalidInteger;
    private Map<String, String> invalidNumber;
    private Map<String, String> defaultValues;

    public EndpointValidationResult() {
        this(null);
    }

    public EndpointValidationResult(String uri) {
        this.uri = uri;
    }

    public String getUri() {
        return uri;
    }

    public boolean hasErrors() {
        return errors > 0;
    }

    public int getNumberOfErrors() {
        return errors;
    }

    public boolean isSuccess() {
        boolean ok = syntaxError == null && unknownComponent == null && incapable == null
                && unknown == null && required == null;
        if (ok) {
            ok = notConsumerOnly == null && notProducerOnly == null;
        }
        if (ok) {
            ok = invalidEnum == null && invalidEnumChoices == null && invalidReference == null
                && invalidBoolean == null && invalidInteger == null && invalidNumber == null;
        }
        return ok;
    }

    public void addSyntaxError(String syntaxError) {
        this.syntaxError = syntaxError;
        errors++;
    }

    public void addIncapable(String uri) {
        this.incapable = uri;
        errors++;
    }

    public void addUnknownComponent(String name) {
        this.unknownComponent = name;
        errors++;
    }

    public void addUnknown(String name) {
        if (unknown == null) {
            unknown = new LinkedHashSet<>();
        }
        if (!unknown.contains(name)) {
            unknown.add(name);
            errors++;
        }
    }

    public void addUnknownSuggestions(String name, String[] suggestions) {
        if (unknownSuggestions == null) {
            unknownSuggestions = new LinkedHashMap<>();
        }
        unknownSuggestions.put(name, suggestions);
    }

    public void addLenient(String name) {
        if (lenient == null) {
            lenient = new LinkedHashSet<>();
        }
        if (!lenient.contains(name)) {
            lenient.add(name);
        }
    }

    public void addRequired(String name) {
        if (required == null) {
            required = new LinkedHashSet<>();
        }
        if (!required.contains(name)) {
            required.add(name);
            errors++;
        }
    }

    public void addInvalidEnum(String name, String value) {
        if (invalidEnum == null) {
            invalidEnum = new LinkedHashMap<>();
        }
        if (!invalidEnum.containsKey(name)) {
            invalidEnum.put(name, value);
            errors++;
        }
    }

    public void addInvalidEnumChoices(String name, String[] choices) {
        if (invalidEnumChoices == null) {
            invalidEnumChoices = new LinkedHashMap<>();
        }
        invalidEnumChoices.put(name, choices);
    }

    public void addInvalidEnumSuggestions(String name, String[] suggestions) {
        if (invalidEnumSuggestions == null) {
            invalidEnumSuggestions = new LinkedHashMap<>();
        }
        invalidEnumSuggestions.put(name, suggestions);
    }

    public void addInvalidReference(String name, String value) {
        if (invalidReference == null) {
            invalidReference = new LinkedHashMap<>();
        }
        if (!invalidReference.containsKey(name)) {
            invalidReference.put(name, value);
            errors++;
        }
    }

    public void addInvalidBoolean(String name, String value) {
        if (invalidBoolean == null) {
            invalidBoolean = new LinkedHashMap<>();
        }
        if (!invalidBoolean.containsKey(name)) {
            invalidBoolean.put(name, value);
            errors++;
        }
    }

    public void addInvalidInteger(String name, String value) {
        if (invalidInteger == null) {
            invalidInteger = new LinkedHashMap<>();
        }
        if (!invalidInteger.containsKey(name)) {
            invalidInteger.put(name, value);
            errors++;
        }
    }

    public void addInvalidNumber(String name, String value) {
        if (invalidNumber == null) {
            invalidNumber = new LinkedHashMap<>();
        }
        if (!invalidNumber.containsKey(name)) {
            invalidNumber.put(name, value);
            errors++;
        }
    }

    public void addDefaultValue(String name, String value)  {
        if (defaultValues == null) {
            defaultValues = new LinkedHashMap<>();
        }
        defaultValues.put(name, value);
    }

    public void addNotConsumerOnly(String name) {
        if (notConsumerOnly == null) {
            notConsumerOnly = new LinkedHashSet<>();
        }
        if (!notConsumerOnly.contains(name)) {
            notConsumerOnly.add(name);
            errors++;
        }
    }

    public void addNotProducerOnly(String name) {
        if (notProducerOnly == null) {
            notProducerOnly = new LinkedHashSet<>();
        }
        if (!notProducerOnly.contains(name)) {
            notProducerOnly.add(name);
            errors++;
        }
    }

    public String getSyntaxError() {
        return syntaxError;
    }

    public String getIncapable() {
        return incapable;
    }

    public Set<String> getUnknown() {
        return unknown;
    }

    public Set<String> getLenient() {
        return lenient;
    }

    public Map<String, String[]> getUnknownSuggestions() {
        return unknownSuggestions;
    }

    public String getUnknownComponent() {
        return unknownComponent;
    }

    public Set<String> getRequired() {
        return required;
    }

    public Map<String, String> getInvalidEnum() {
        return invalidEnum;
    }

    public Map<String, String[]> getInvalidEnumChoices() {
        return invalidEnumChoices;
    }

    public List<String> getEnumChoices(String optionName) {
        if (invalidEnumChoices != null) {
            String[] enums = invalidEnumChoices.get(optionName);
            if (enums != null) {
                return Arrays.asList(enums);
            }
        }

        return Collections.emptyList();
    }

    public Map<String, String> getInvalidReference() {
        return invalidReference;
    }

    public Map<String, String> getInvalidBoolean() {
        return invalidBoolean;
    }

    public Map<String, String> getInvalidInteger() {
        return invalidInteger;
    }

    public Map<String, String> getInvalidNumber() {
        return invalidNumber;
    }

    public Map<String, String> getDefaultValues() {
        return defaultValues;
    }

    public Set<String> getNotConsumerOnly() {
        return notConsumerOnly;
    }

    public Set<String> getNotProducerOnly() {
        return notProducerOnly;
    }

    /**
     * A human readable summary of the validation errors.
     *
     * @param includeHeader whether to include a header
     * @return the summary, or <tt>null</tt> if no validation errors
     */
    public String summaryErrorMessage(boolean includeHeader) {
        if (isSuccess()) {
            return null;
        }

        if (incapable != null) {
            return "\tIncapable of parsing uri: " + incapable;
        } else if (syntaxError != null) {
            return "\tSyntax error: " + syntaxError;
        } else if (unknownComponent != null) {
            return "\tUnknown component: " + unknownComponent;
        }

        // for each invalid option build a reason message
        Map<String, String> options = new LinkedHashMap<>();
        if (unknown != null) {
            for (String name : unknown) {
                if (unknownSuggestions != null && unknownSuggestions.containsKey(name)) {
                    String[] suggestions = unknownSuggestions.get(name);
                    if (suggestions != null && suggestions.length > 0) {
                        String str = Arrays.asList(suggestions).toString();
                        options.put(name, "Unknown option. Did you mean: " + str);
                    } else {
                        options.put(name, "Unknown option");
                    }
                } else {
                    options.put(name, "Unknown option");
                }
            }
        }
        if (notConsumerOnly != null) {
            for (String name : notConsumerOnly) {
                options.put(name, "Option not applicable in consumer only mode");
            }
        }
        if (notProducerOnly != null) {
            for (String name : notProducerOnly) {
                options.put(name, "Option not applicable in producer only mode");
            }
        }
        if (required != null) {
            for (String name : required) {
                options.put(name, "Missing required option");
            }
        }
        if (invalidEnum != null) {
            for (Map.Entry<String, String> entry : invalidEnum.entrySet()) {
                String name = entry.getKey();
                String[] choices = invalidEnumChoices.get(name);
                String defaultValue = defaultValues != null ? defaultValues.get(entry.getKey()) : null;
                String str = Arrays.asList(choices).toString();
                String msg = "Invalid enum value: " + entry.getValue() + ". Possible values: " + str;
                if (invalidEnumSuggestions != null) {
                    String[] suggestions = invalidEnumSuggestions.get(name);
                    if (suggestions != null && suggestions.length > 0) {
                        str = Arrays.asList(suggestions).toString();
                        msg += ". Did you mean: " + str;
                    }
                }
                if (defaultValue != null) {
                    msg += ". Default value: " + defaultValue;
                }

                options.put(entry.getKey(), msg);
            }
        }
        if (invalidReference != null) {
            for (Map.Entry<String, String> entry : invalidReference.entrySet()) {
                boolean empty = isEmpty(entry.getValue());
                if (empty) {
                    options.put(entry.getKey(), "Empty reference value");
                } else if (!entry.getValue().startsWith("#")) {
                    options.put(entry.getKey(), "Invalid reference value: " + entry.getValue() + " must start with #");
                } else {
                    options.put(entry.getKey(), "Invalid reference value: " + entry.getValue());
                }
            }
        }
        if (invalidBoolean != null) {
            for (Map.Entry<String, String> entry : invalidBoolean.entrySet()) {
                boolean empty = isEmpty(entry.getValue());
                if (empty) {
                    options.put(entry.getKey(), "Empty boolean value");
                } else {
                    options.put(entry.getKey(), "Invalid boolean value: " + entry.getValue());
                }
            }
        }
        if (invalidInteger != null) {
            for (Map.Entry<String, String> entry : invalidInteger.entrySet()) {
                boolean empty = isEmpty(entry.getValue());
                if (empty) {
                    options.put(entry.getKey(), "Empty integer value");
                } else {
                    options.put(entry.getKey(), "Invalid integer value: " + entry.getValue());
                }
            }
        }
        if (invalidNumber != null) {
            for (Map.Entry<String, String> entry : invalidNumber.entrySet()) {
                boolean empty = isEmpty(entry.getValue());
                if (empty) {
                    options.put(entry.getKey(), "Empty number value");
                } else {
                    options.put(entry.getKey(), "Invalid number value: " + entry.getValue());
                }
            }
        }

        // build a table with the error summary nicely formatted
        // lets use 24 as min length
        int maxLen = 24;
        for (String key : options.keySet()) {
            maxLen = Math.max(maxLen, key.length());
        }
        String format = "%" + maxLen + "s    %s";

        // build the human error summary
        StringBuilder sb = new StringBuilder();
        if (includeHeader) {
            sb.append("Endpoint validator error\n");
            sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n");
            sb.append("\n");
        }
        if (uri != null) {
            sb.append("\t").append(uri).append("\n");
        } else {
            sb.append("\n");
        }
        for (Map.Entry<String, String> option : options.entrySet()) {
            String out = String.format(format, option.getKey(), option.getValue());
            sb.append("\n\t").append(out);
        }

        return sb.toString();
    }
}
