/*
 *  Copyright 2017 SmartBear Software
 *
 *  Licensed 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.mockserver.openapi.examples;

import io.swagger.util.Json;
import io.swagger.v3.oas.models.media.*;
import org.apache.commons.lang3.StringUtils;
import org.mockserver.log.model.LogEntry;
import org.mockserver.logging.MockServerLogger;
import org.mockserver.openapi.examples.models.*;

import java.io.IOException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;

import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * See: https://github.com/swagger-api/swagger-inflector
 */
@SuppressWarnings("rawtypes")
public class ExampleBuilder {
    private static final MockServerLogger MOCK_SERVER_LOGGER = new MockServerLogger(ExampleBuilder.class);

    public static final String SAMPLE_EMAIL_PROPERTY_VALUE = "some_email@mockserver.com";
    public static final String SAMPLE_UUID_PROPERTY_VALUE = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
    public static final String SAMPLE_STRING_PROPERTY_VALUE = "some_string_value";
    public static final int SAMPLE_INT_PROPERTY_VALUE = 0;
    public static final int SAMPLE_LONG_PROPERTY_VALUE = 0;
    public static final int SAMPLE_BASE_INTEGER_PROPERTY_VALUE = 0;
    public static final float SAMPLE_FLOAT_PROPERTY_VALUE = 1.1f;
    public static final double SAMPLE_DOUBLE_PROPERTY_VALUE = 1.1f;
    public static final boolean SAMPLE_BOOLEAN_PROPERTY_VALUE = true;
    public static final String SAMPLE_BYTE_PROPERTY_VALUE = "c29tZV9ieXRlX3ZhbHVl";
    public static final String SAMPLE_DATE_PROPERTY_VALUE = "2018-11-13";
    public static final String SAMPLE_TIME_PROPERTY_VALUE = "20:20:39+00:00";
    public static final String SAMPLE_DATETIME_PROPERTY_VALUE = SAMPLE_DATE_PROPERTY_VALUE + "T" + SAMPLE_TIME_PROPERTY_VALUE;
    public static final double SAMPLE_DECIMAL_PROPERTY_VALUE = 1.5;

    public static Example fromSchema(Schema<?> property, Map<String, Schema> definitions) {
        return fromProperty(null, property, definitions, new ConcurrentHashMap<>(), new ConcurrentSkipListSet<>(), new StringBuilder());
    }

    public static Example fromProperty(String name, Schema<?> property, Map<String, Schema> definitions, Map<String, Example> processedModels, Set<String> modelsStartedProcessing, StringBuilder location) {
        location = new StringBuilder(location);
        if (isNotBlank(name)) {
            location.append(name).append(".");
        }

        if (property == null) {
            return null;
        }

        // name = null;
        String namespace = null;
        String prefix = null;
        Boolean attribute = false;
        boolean wrapped = false;

        if (property.getXml() != null) {
            XML xml = property.getXml();
            name = xml.getName();
            namespace = xml.getNamespace();
            prefix = xml.getPrefix();
            attribute = xml.getAttribute();
            wrapped = xml.getWrapped() != null ? xml.getWrapped() : false;
        }

        Example output = null;

        Object example = property.getExample();

        if (property.get$ref() != null) {
            String ref = property.get$ref();
            ref = ref.substring(ref.lastIndexOf("/") + 1);
            if (modelsStartedProcessing.contains(ref) && !processedModels.containsKey(ref)) {
                MOCK_SERVER_LOGGER.logEvent(
                    new LogEntry()
                        .setMessageFormat("unable to create example for{}due to forward reference or circular reference")
                        .setArguments(StringUtils.substringBeforeLast(location.toString(), "."))
                );
                return null;
            }
            modelsStartedProcessing.add(ref);
            if (processedModels.containsKey(ref)) {
                // return some sort of example
                output = alreadyProcessedRefExample(ref, definitions, processedModels);
            } else if (definitions != null) {
                Schema<?> model = definitions.get(ref);
                if (model != null) {
                    output = fromProperty(ref, model, definitions, processedModels, modelsStartedProcessing, location);
                    processedModels.put(ref, output);
                }
            }
        } else if (property instanceof EmailSchema emailSchema) {
            if (example != null) {
                output = new StringExample(example.toString());
            } else {
                String defaultValue = emailSchema.getDefault();

                if (defaultValue == null) {
                    List<String> enums = emailSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }

                output = new StringExample(defaultValue == null ? SAMPLE_EMAIL_PROPERTY_VALUE : defaultValue);
            }
        } else if (property instanceof UUIDSchema uuidSchema) {
            if (example != null) {
                output = new StringExample(example.toString());
            } else {
                UUID defaultValue = uuidSchema.getDefault();

                if (defaultValue == null) {
                    List<UUID> enums = uuidSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }

                output = new StringExample(defaultValue == null ? SAMPLE_UUID_PROPERTY_VALUE : defaultValue.toString());
            }
        } else if (property instanceof ByteArraySchema) {
            if (example != null) {
                output = new StringExample(example.toString());
            } else {
                output = new StringExample(SAMPLE_BYTE_PROPERTY_VALUE);
            }
        } else if (property instanceof StringSchema stringSchema) {
            if (example != null) {
                output = new StringExample(example.toString());
            } else {
                String defaultValue = stringSchema.getDefault();

                if (defaultValue == null) {
                    List<String> enums = stringSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }

                output = new StringExample(defaultValue == null ? SAMPLE_STRING_PROPERTY_VALUE : defaultValue);
            }
        } else if (property instanceof PasswordSchema passwordSchema) {
            if (example != null) {
                output = new StringExample(example.toString());
            } else {
                String defaultValue = passwordSchema.getDefault();

                if (defaultValue == null) {
                    List<String> enums = passwordSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }

                output = new StringExample(defaultValue == null ? SAMPLE_STRING_PROPERTY_VALUE : defaultValue);
            }
        } else if (property instanceof IntegerSchema integerSchema) {
            if (example != null) {
                try {
                    if (property.getFormat() != null) {
                        if (property.getFormat().equals("int32")) {
                            output = new IntegerExample(Integer.parseInt(example.toString()));
                        } else if (property.getFormat().equals("int64")) {
                            output = new LongExample(Long.parseLong(example.toString()));
                        }
                    } else {
                        output = new IntegerExample(Integer.parseInt(example.toString()));
                    }
                } catch (NumberFormatException ignored) {
                }
            }

            if (output == null) {
                Number defaultValue = integerSchema.getDefault();

                if (defaultValue == null) {
                    List<Number> enums = integerSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }
                if (property.getFormat() != null) {
                    if (property.getFormat().equals("int32")) {
                        output = new IntegerExample(defaultValue == null ? SAMPLE_INT_PROPERTY_VALUE : defaultValue.intValue());
                    } else if (property.getFormat().equals("int64")) {
                        output = new LongExample(defaultValue == null ? SAMPLE_LONG_PROPERTY_VALUE : defaultValue.longValue());
                    }
                } else {
                    output = new IntegerExample(SAMPLE_BASE_INTEGER_PROPERTY_VALUE);
                }
            }
        } else if (property instanceof NumberSchema numberSchema) {

            if (example != null) {
                try {
                    if (property.getFormat() != null) {
                        if (property.getFormat().equals("double")) {
                            output = new DoubleExample(Double.parseDouble(example.toString()));
                        } else if (property.getFormat().equals("float")) {
                            output = new FloatExample(Float.parseFloat(example.toString()));
                        }
                    } else {
                        output = new DecimalExample(new BigDecimal(example.toString()));
                    }
                } catch (NumberFormatException ignored) {
                }
            }

            if (output == null) {
                BigDecimal defaultValue = numberSchema.getDefault();

                if (defaultValue == null) {
                    List<BigDecimal> enums = numberSchema.getEnum();
                    if (enums != null && !enums.isEmpty()) {
                        defaultValue = enums.get(0);
                    }
                }
                if (property.getFormat() != null) {
                    if (property.getFormat().equals("double")) {
                        output = new DoubleExample(defaultValue == null ? SAMPLE_DOUBLE_PROPERTY_VALUE : defaultValue.doubleValue());
                    }
                    if (property.getFormat().equals("float")) {
                        output = new FloatExample(defaultValue == null ? SAMPLE_FLOAT_PROPERTY_VALUE : defaultValue.floatValue());
                    }
                } else {
                    output = new DecimalExample(new BigDecimal(SAMPLE_DECIMAL_PROPERTY_VALUE));
                }
            }

        } else if (property instanceof BooleanSchema) {
            if (example != null) {
                output = new BooleanExample(Boolean.parseBoolean(example.toString()));
            } else {
                Boolean defaultValue = (Boolean) property.getDefault();
                output = new BooleanExample(defaultValue == null ? SAMPLE_BOOLEAN_PROPERTY_VALUE : defaultValue);
            }
        } else if (property instanceof DateSchema dateSchema) {
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            if (example != null) {
                String exampleAsString = format.format(example);
                output = new StringExample(exampleAsString);
            } else {

                List<Date> enums = dateSchema.getEnum();
                if (enums != null && !enums.isEmpty()) {
                    output = new StringExample(format.format(enums.get(0)));
                } else {
                    output = new StringExample(SAMPLE_DATE_PROPERTY_VALUE);
                }
            }
        } else if (property instanceof DateTimeSchema dateTimeSchema) {
            if (example != null) {
                String exampleAsString = example.toString();
                output = new StringExample(exampleAsString);
            } else {
                List<OffsetDateTime> enums = dateTimeSchema.getEnum();
                if (enums != null && !enums.isEmpty()) {
                    output = new StringExample(enums.get(0).toString());
                } else {
                    output = new StringExample(SAMPLE_DATETIME_PROPERTY_VALUE);
                }
            }
        } else if (property instanceof ObjectSchema objectSchema) {
            if (example != null) {
                try {
                    output = Json.mapper().readValue(example.toString(), ObjectExample.class);
                } catch (IOException e) {
                    MOCK_SERVER_LOGGER.logEvent(
                        new LogEntry()
                            .setMessageFormat("unable to convert{}to JsonNode")
                            .setArguments(example)
                            .setArguments(example)
                    );
                    output = new ObjectExample();
                }
            } else {
                ObjectExample outputExample = new ObjectExample();
                outputExample.setName(property.getName());
                if (objectSchema.getProperties() != null) {
                    for (String propertyname : objectSchema.getProperties().keySet()) {
                        Schema<?> inner = objectSchema.getProperties().get(propertyname);
                        Example innerExample = fromProperty(propertyname, inner, definitions, processedModels, modelsStartedProcessing, location);
                        outputExample.put(propertyname, innerExample);
                    }
                    output = outputExample;
                }

            }
        } else if (property instanceof ArraySchema arraySchema) {
            if (example != null) {
                try {
                    output = Json.mapper().readValue(example.toString(), ArrayExample.class);
                } catch (IOException e) {
                    MOCK_SERVER_LOGGER.logEvent(
                        new LogEntry()
                            .setMessageFormat("unable to create example for{}because unable to convert{}to JsonNode")
                            .setArguments(StringUtils.substringBeforeLast(location.toString(), "."))
                            .setArguments(example)
                    );
                    output = new ArrayExample();
                }
            } else {
                Schema<?> inner = arraySchema.getItems();
                if (inner != null) {
                    Example innerExample = fromProperty(property.getType(), inner, definitions, processedModels, modelsStartedProcessing, location);
                    if (innerExample != null) {
                        ArrayExample an = new ArrayExample();
                        an.add(innerExample);
                        an.setName(property.getName());
                        output = an;
                    }
                }
            }
        } else if (property instanceof ComposedSchema composedSchema) {
            if (composedSchema.getAllOf() != null) {
                List<Schema> models = composedSchema.getAllOf();
                ObjectExample ex = new ObjectExample();
                List<Example> innerExamples = new ArrayList<>();
                if (models != null) {
                    for (Schema im : models) {
                        Example innerExample = fromProperty(im.getType(), im, definitions, processedModels, modelsStartedProcessing, location);
                        if (innerExample != null) {
                            innerExamples.add(innerExample);
                        }
                    }
                }
                if (composedSchema.getProperties() != null) {
                    Map<String, Schema> ownProperties = composedSchema.getProperties();
                    for (Map.Entry<String, Schema> entry : ownProperties.entrySet()) {
                        Example propExample = fromProperty(entry.getKey(), entry.getValue(), definitions, processedModels, modelsStartedProcessing, location);
                        if (propExample != null) {
                            ex.put(entry.getKey(), propExample);
                        }
                    }
                }
                mergeTo(ex, innerExamples);
                output = ex;
            }
            if (composedSchema.getAnyOf() != null) {
                List<Schema> models = composedSchema.getAnyOf();
                if (models != null) {
                    for (Schema im : models) {
                        Example innerExample = fromProperty(property.getType(), im, definitions, processedModels, modelsStartedProcessing, location);
                        if (innerExample != null) {
                            output = innerExample;
                            break;
                        }
                    }
                }
            }
            if (composedSchema.getOneOf() != null) {
                List<Schema> models = composedSchema.getOneOf();
                if (models != null) {
                    for (Schema im : models) {
                        Example innerExample = fromProperty(property.getType(), im, definitions, processedModels, modelsStartedProcessing, location);
                        if (innerExample != null) {
                            output = innerExample;
                            break;
                        }
                    }
                }
            }
        } else if (property.getProperties() != null) {
            if (example != null) {
                try {
                    output = Json.mapper().readValue(example.toString(), ObjectExample.class);
                } catch (IOException e) {
                    MOCK_SERVER_LOGGER.logEvent(
                        new LogEntry()
                            .setMessageFormat("unable to convert{}to JsonNode")
                            .setArguments(example)
                    );
                    output = new ObjectExample();
                }
            } else {
                ObjectExample ex = new ObjectExample();

                if (property.getProperties() != null) {
                    Map<String, Schema> properties = property.getProperties();
                    for (String propertyKey : properties.keySet()) {
                        Schema inner = properties.get(propertyKey);
                        Example propExample = fromProperty(propertyKey, inner, definitions, processedModels, modelsStartedProcessing, location);
                        ex.put(propertyKey, propExample);
                    }
                }

                output = ex;
            }

        }
        if (property.getAdditionalProperties() instanceof Schema<?> inner) {
            if (inner != null) {
                for (int i = 1; i <= 3; i++) {
                    Example innerExample = fromProperty(inner.getType(), inner, definitions, processedModels, modelsStartedProcessing, location);
                    if (innerExample != null) {
                        if (output == null) {
                            output = new ObjectExample();
                        }
                        ObjectExample on = null;
                        if (output instanceof ObjectExample objectExample) {
                            on = objectExample;
                        }
                        String key = "additionalProp" + i;
                        if (innerExample.getName() == null) {
                            innerExample.setName(key);
                        }

                        if (on != null && !on.keySet().contains(key)) {
                            on.put(key, innerExample);
                        }
                    }
                }
            }
        } else if (property.getAdditionalProperties() instanceof Boolean && output == null) {
            output = new ObjectExample();
        }
        if (output != null) {
            if (attribute != null) {
                output.setAttribute(attribute);
            }
            if (wrapped) {
                if (name != null) {
                    output.setWrappedName(name);
                }
            } else if (name != null) {
                output.setName(name);
            }
            output.setNamespace(namespace);
            output.setPrefix(prefix);
            output.setWrapped(wrapped);
        }
        return output;
    }

    public static Example alreadyProcessedRefExample(String name, Map<String, Schema> definitions, Map<String, Example> processedModels) {
        if (processedModels.get(name) != null) {
            return processedModels.get(name);
        }
        Schema<?> model = definitions.get(name);
        if (model == null) {
            return null;
        }
        Example output = null;

        // look at type
        if (model.getType() != null) {
            if ("object".equals(model.getType())) {
                return new ObjectExample();
            } else if ("string".equals(model.getType())) {
                return new StringExample("");
            } else if ("integer".equals(model.getType())) {
                return new IntegerExample(0);
            } else if ("long".equals(model.getType())) {
                return new LongExample(0);
            } else if ("float".equals(model.getType())) {
                return new FloatExample(0);
            } else if ("double".equals(model.getType())) {
                return new DoubleExample(0);
            }
        }

        return output;
    }


    public static void mergeTo(ObjectExample output, List<Example> examples) {
        for (Example ex : examples) {
            if (ex instanceof ObjectExample objectExample) {
                Map<String, Example> values = objectExample.getValues();
                if (values != null) {
                    output.putAll(values);
                }
            }
        }
    }
}