package ru.yandex.webmaster3.core.http.autodoc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.base.Joiner;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.autodoc.common.doc.DocUtils;
import ru.yandex.autodoc.common.out.json.JsonConvertableUtils;
import ru.yandex.autodoc.common.out.json.JsonObjectWriter;
import ru.yandex.autodoc.common.out.json.JsonValueWriter;
import ru.yandex.autodoc.common.out.json.builder.ContainerBuilder;
import ru.yandex.autodoc.common.out.json.builder.JsonNoTypePolymorphicBuilder;
import ru.yandex.autodoc.common.out.json.builder.JsonObjectBuilder;
import ru.yandex.autodoc.common.out.json.builder.JsonValueBuilder;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.ActionResponse;
import ru.yandex.webmaster3.core.http.HierarchyTypeField;
import ru.yandex.webmaster3.core.util.ReflectionUtils;
import ru.yandex.webmaster3.core.util.json.polymorphic.Discriminator;
import ru.yandex.webmaster3.core.util.json.polymorphic.Polymorphic;

/**
 * @author avhaliullin
 */
public class ClassShapeRetriever {
    private static final Logger log = LoggerFactory.getLogger(ClassShapeRetriever.class);
    private static final String ERROR_CODE_FIELD_NAME = "code";
    private static final Set<String> ERROR_OBJECT_HIDE_FIELDS = new HashSet<String>() {{
        add("class");
        add("message");
        add("stackTrace");
        add("subsystem");
    }};
    private static final JsonValueWriter EMPTY_ARRAY = new JsonValueWriter() {
        @Override
        public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
            return builder.valueArray().endArray();
        }
    };

    private final ObjectFieldsRetriever objectRetriever;
    private final ObjectMapper objectMapper;
    private final ObjectType objectType;
    private final MapperConfig mapperConfig;
    private final RetrieveMethod defaultRetrieveMethod;

    public ClassShapeRetriever(ObjectMapper objectMapper, ObjectType objectType) {
        this.objectMapper = objectMapper;
        this.objectType = objectType;
        switch (objectType) {
            case REQUEST:
                this.mapperConfig = objectMapper.getDeserializationConfig();
                this.objectRetriever = new RequestFieldsRetriever(objectMapper);
                this.defaultRetrieveMethod = RetrieveMethod.ANY;
                break;
            case RESPONSE:
                this.mapperConfig = objectMapper.getSerializationConfig();
                this.objectRetriever = new ResponseFieldsRetriever(objectMapper);
                this.defaultRetrieveMethod = RetrieveMethod.RESPONSE_NOT_ERROR;
                break;
            default:
                throw new RuntimeException("Unknown object type " + objectType);
        }
    }

    private boolean isImplementation(Class<?> baseClass, Class<?> candidate, RetrieveMethod retrieveMethod) {
        boolean fitsCommonConditions = baseClass.isAssignableFrom(candidate) &&
                !Modifier.isAbstract(candidate.getModifiers());
        if (fitsCommonConditions) {
            switch (retrieveMethod) {
                case RESPONSE_NOT_ERROR:
                    return !ActionResponse.ErrorResponse.class.isAssignableFrom(candidate);
                case RESPONSE_ONLY_ERROR:
                    return ActionResponse.ErrorResponse.class.isAssignableFrom(candidate);
                case ANY:
                    return true;
                default:
                    throw new RuntimeException("Unknown retrieve method: " + retrieveMethod);
            }
        } else {
            return false;
        }
    }

    private List<Pair<FullTypeInfo, String>> getImplementationsForPolymorphic(FullTypeInfo type) {
        FullTypeInfo polyType = type.ancestorFullType(Polymorphic.class);
        FullTypeInfo discriminatorType = polyType.getGenericsList().get(0);
        Class<Enum> enumClass = (Class<Enum>) discriminatorType.getClazz();
        Comparator enumComparator = EnumComparators.createComparator(enumClass);
        List<Enum> enumValues = Arrays.asList(enumClass.getEnumConstants());
        Collections.sort(enumValues, enumComparator);
        List<Pair<FullTypeInfo, String>> result = new ArrayList<>();
        for (Enum discriminator : enumValues) {
            Class<?> descendant = ((Discriminator) discriminator).getDataClass();
            if (type.getClazz().isAssignableFrom(descendant)) {
                result.add(Pair.of(FullTypeInfo.createSimple(descendant), discriminator.name()));
            }
        }
        return result;
    }

    private List<FullTypeInfo> getImplementations(FullTypeInfo type, RetrieveMethod retrieveMethod) {
        List<FullTypeInfo> result = new ArrayList<>();
        for (Class<?> candidate : type.getClazz().getDeclaredClasses()) {
            if (isImplementation(type.getClazz(), candidate, retrieveMethod)) {
                result.add(FullTypeInfo.createSimple(candidate));
            }
        }
        if (isImplementation(type.getClazz(), type.getClazz(), retrieveMethod)) {
            result.add(type);
        }
        return result;
    }

    private String hierarchyTypeField(FullTypeInfo type) {
        for (FieldRepresentation field : objectRetriever.getFields(type)) {
            if (field.annotatedMember.hasAnnotation(HierarchyTypeField.class)) {
                return field.name;
            }
        }
        return null;
    }

    private JsonExample retrieveCustomJson(final FullTypeInfo type) {
        if (JsonNode.class.isAssignableFrom(type.getClazz())) {
            if (type.getClazz().equals(ObjectNode.class)) {
                return new JsonExample(JsonConvertableUtils.EMPTY_OBJECT, "custom json object");
            } else if (type.getClazz().equals(ArrayNode.class)) {
                return new JsonExample(EMPTY_ARRAY, "custom json array");
            } else if (NumericNode.class.isAssignableFrom(type.getClazz())) {
                return retrieveValueClass(Integer.class);
            } else if (type.getClazz().equals(TextNode.class)) {
                return retrieveValueClass(String.class);
            } else {
                return new JsonExample(JsonConvertableUtils.NULL_WRITER, "custom json");
            }
        }
        return null;
    }

    private JsonObjectWriter retrieveSimpleObject(final FullTypeInfo type, final Set<Type> usedTypes,
                                                  final RetrieveMethod retrieveMethod, final String typeField, final String typeFieldValue) {
        final List<FieldRepresentation> fields = objectRetriever.getFields(type);
        Collections.sort(fields);
        final boolean doRender = !usedTypes.contains(type.getClazz());
        return new JsonObjectWriter() {
            @Override
            public <C extends ContainerBuilder> JsonObjectBuilder<C> writeObject(JsonObjectBuilder<C> builder) {
                if (doRender) {
                    boolean injectTypeField = typeField != null && typeFieldValue != null;
                    for (FieldRepresentation field : fields) {
                        if (retrieveMethod == RetrieveMethod.RESPONSE_ONLY_ERROR && ERROR_OBJECT_HIDE_FIELDS.contains(field.name)) {
                            continue;
                        }
                        Set<Type> newUsedTypes = new HashSet<>(usedTypes);
                        newUsedTypes.add(type.getClazz());
                        JsonExample fieldExample = null;
                        if (field.name.equals(typeField)) {
                            injectTypeField = false;
                        }
                        if (field.name.equals(typeField) && typeFieldValue != null) {
                            fieldExample = new JsonExample(JsonConvertableUtils.stringWriter(typeFieldValue));
                        } else {
                            if (
                                    (field.name.equals(typeField) ||
                                            (retrieveMethod == RetrieveMethod.RESPONSE_ONLY_ERROR && field.name.equals(ERROR_CODE_FIELD_NAME))) &&
                                            !Modifier.isAbstract(type.getClazz().getModifiers())
                                    ) {
                                try {
                                    Object obj = ReflectionUtils.instantiateWithDefaults(type.getClazz());
                                    fieldExample = new JsonExample(JsonConvertableUtils.stringWriter(((Method) field.member).invoke(obj).toString()));
                                } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
                                    log.warn("Failed to get value of field, annotated with HierarchyTypeField: {}",
                                            field.name, e);
                                }
                            }
                        }
                        if (fieldExample == null) {
                            fieldExample = retrieve(field.type, newUsedTypes, RetrieveMethod.ANY);
                        }
                        builder = builder
                                .key(field.name, renderComments(fieldExample.comments, field.description, field.nullable ? "Nullable" : null))
                                .writeValue(fieldExample.writer);
                    }
                    if (injectTypeField) {
                        builder = builder.key(typeField).value(typeFieldValue);
                    }
                }
                return builder;
            }
        };
    }

    private JsonExample retrieveMap(FullTypeInfo type, final Set<Type> usedTypes) {
        if (Map.class.isAssignableFrom(type.getClazz())) {
            List<FullTypeInfo> generics = type.ancestorFullType(Map.class).getGenericsList();
            FullTypeInfo keyType = generics.get(0);
            FullTypeInfo valueType = generics.get(1);
            if (!keyType.getClazz().isEnum()) {
                log.warn("Maps with non-enum keys are not supported!");
                return new JsonExample(JsonConvertableUtils.EMPTY_OBJECT);
            }
            String key = "${description(" + keyType.getClazz().getName() + ")}";
            JsonExample valueExample = retrieve(valueType, usedTypes, RetrieveMethod.ANY);
            return new JsonExample(new JsonValueWriter() {
                @Override
                public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
                    return builder
                            .valueObject()
                            .key(key, renderComments(valueExample.comments)).writeValue(valueExample.writer)
                            .endObject(true);
                }
            });
        }
        return null;
    }

    private JsonExample retrieveObject(FullTypeInfo type, final Set<Type> usedTypes, RetrieveMethod retrieveMethod) {
        type = forceConcrete(type, retrieveMethod);
        final Class<?> clazz = type.getClazz();
        final List<JsonObjectWriter> implementationReprs = new ArrayList<>();
        final List<String> implementationNames = new ArrayList<>();
        if (Polymorphic.class.isAssignableFrom(clazz)) {
            List<Pair<FullTypeInfo, String>> implementations = getImplementationsForPolymorphic(type);
            for (Pair<FullTypeInfo, String> pair : implementations) {
                implementationReprs.add(retrieveSimpleObject(pair.getLeft(), usedTypes, retrieveMethod, "type", pair.getRight()));
                implementationNames.add(pair.getLeft().getClazz().getSimpleName());
            }
        } else {
            final List<FullTypeInfo> implementations = getImplementations(type, retrieveMethod);
            if (implementations.isEmpty()) {
                implementations.add(type);
            }
            String typeField = hierarchyTypeField(type);

            for (FullTypeInfo impl : implementations) {
                implementationReprs.add(retrieveSimpleObject(impl, usedTypes, retrieveMethod, typeField, null));
                implementationNames.add(impl.getClazz().getSimpleName());
            }
        }
        if (implementationReprs.size() == 1) {
            final JsonObjectWriter objectWriter = implementationReprs.get(0);
            if (objectWriter == null) {
                return new JsonExample(null);
            } else {
                return new JsonExample(new JsonValueWriter() {
                    @Override
                    public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
                        return builder.valueObject().comment(clazz.getSimpleName()).writeObject(objectWriter).endObject();
                    }
                });
            }
        } else {
            return new JsonExample(new JsonValueWriter() {
                @Override
                public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
                    JsonNoTypePolymorphicBuilder<C> pb = builder.polymorphic();
                    for (int i = 0; i < implementationReprs.size(); i++) {
                        JsonObjectWriter impl = implementationReprs.get(i);
                        if (impl != null) {
                            String name = implementationNames.get(i);
                            pb.withCase(name, impl);
                        }
                    }
                    return pb.endPolymorphic();
                }
            });
        }
    }

    public List<WebmasterErrorDescription> retrieveErrors(Type type) {
        return retrieveErrors(FullTypeInfo.createFromAny(type));
    }

    public List<WebmasterErrorDescription> retrieveErrors(FullTypeInfo type) {
        List<FullTypeInfo> errorImplementations = getImplementations(type, RetrieveMethod.RESPONSE_ONLY_ERROR);
        List<WebmasterErrorDescription> result = new ArrayList<>();
        for (FullTypeInfo impl : errorImplementations) {
            JsonValueWriter errorWriter = retrieve(impl, RetrieveMethod.RESPONSE_ONLY_ERROR);
            ActionResponse.ErrorResponse example = (ActionResponse.ErrorResponse) ReflectionUtils.instantiateWithDefaults(impl.getClazz());
            String desciption = AutodocUtil.getClassDescription(impl.getClazz());
            if (example.getCode() == null) {
                throw new RuntimeException("Code is null for error class " + impl.getClazz().getName());
            }
            result.add(new WebmasterErrorDescription(example.getCode().toString(), desciption, errorWriter, example.getSubsystem()));
        }
        return result;
    }

    public JsonValueWriter retrieve(FullTypeInfo type) {
        return retrieve(type, defaultRetrieveMethod);
    }

    protected JsonValueWriter retrieve(FullTypeInfo type, RetrieveMethod retrieveMethod) {
        JsonExample example = retrieve(type, new LinkedHashSet<>(), retrieveMethod);
        if (example.comments == null || example.comments.length == 0) {
            return example.writer;
        } else {
            return new JsonValueWriter() {
                @Override
                public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
                    builder.comment(renderComments(example.comments));
                    return example.writer.writeValue(builder);
                }
            };
        }
    }

    protected JsonExample retrieve(FullTypeInfo type, Set<Type> usedTypes, RetrieveMethod retrieveMethod) {
        type = forceConcrete(type, retrieveMethod);
        JsonExample result = retrieveCustomJson(type);
        if (result == null) {
            result = retrieveValueClass(type.getClazz());
        }
        if (result == null) {
            result = retrieveCollectionClass(type, usedTypes);
        }
        if (result == null) {
            result = retrieveMap(type, usedTypes);
        }
        if (result == null) {
            result = retrieveObject(type, usedTypes, retrieveMethod);
        }
        if (result == null) {
            throw new RuntimeException("Unknown type " + type + ". Access stack:\n" + Joiner.on('\n').join(usedTypes));
        } else {
            return result;
        }
    }

    protected JsonExample retrieveCollectionClass(FullTypeInfo type, Set<Type> usedTypes) {
        try {
            Class<?> clazz = type.getClazz();
            FullTypeInfo elementType;
            if (clazz.isArray()) {
                elementType = FullTypeInfo.createFromAny(clazz.getComponentType());
            } else if (Iterable.class.isAssignableFrom(type.getClazz())) {
                elementType = type.ancestorFullType(Iterable.class).getGenericsList().get(0);
            } else {
                return null;
            }
            final JsonExample elementExample;
            if (elementType.equals(type)) {
                elementExample = new JsonExample(EMPTY_ARRAY, "recursive collection type");
            } else {
                elementExample = retrieve(elementType, usedTypes, RetrieveMethod.ANY);
            }
            return new JsonExample(new JsonValueWriter() {
                @Override
                public <C extends ContainerBuilder> C writeValue(JsonValueBuilder<C> builder) {
                    return builder.valueArray().element().writeValue(elementExample.writer).endArray();
                }
            }, elementExample.comments);
        } catch (RuntimeException e) {
            throw new RuntimeException("Error: {" + e.getMessage() + "} happened at:\n" + Joiner.on('\n').join(usedTypes), e);
        }
    }

    private static final UUID UUID_EXAMPLE = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
    private static final DateTime DATE_TIME_EXAMPLE = DateTime.parse("2014-01-01T00:00:00");
    private static final LocalDate LOCAL_DATE_EXAMPLE = DATE_TIME_EXAMPLE.toLocalDate();
    private static final Date JAVA_DATE_EXAMPLE = new Date(1410165814000L);

    protected JsonExample retrieveValueClass(Class<?> clazz) {
        JsonExample result = retrievePrimitive(clazz);
        if (result != null) {
            return result;
        } else if (clazz == String.class) {
            return new JsonExample(JsonConvertableUtils.stringWriter("some string"));
        } else if (clazz == URL.class || clazz == URI.class) {
            return new JsonExample(JsonConvertableUtils.stringWriter("http://www.yandex.ru"));
        } else if (Instant.class.isAssignableFrom(clazz) || DateTime.class.isAssignableFrom(clazz)) {
            return new JsonExample(JsonConvertableUtils.stringWriter(DATE_TIME_EXAMPLE.toString()));
        } else if (LocalDate.class == clazz) {
            return new JsonExample(JsonConvertableUtils.stringWriter(LOCAL_DATE_EXAMPLE.toString()));
        } else if (Date.class.isAssignableFrom(clazz)) {
            return new JsonExample(JsonConvertableUtils.stringWriter(mapperConfig.getDateFormat().format(JAVA_DATE_EXAMPLE)));
        } else if (clazz == UUID.class) {
            return new JsonExample(JsonConvertableUtils.stringWriter(UUID_EXAMPLE.toString()));
        } else if (Class.class == clazz) {
            return new JsonExample(JsonConvertableUtils.stringWriter("ru.yandex.example.Class"));
        } else if (WebmasterHostId.class.isAssignableFrom(clazz)) {
            return new JsonExample(JsonConvertableUtils.stringWriter("http:lenta.ru:80"));
        } else if (Enum.class.isAssignableFrom(clazz)) {
            if (!clazz.isEnum()) {
                // Плохи дела - абстрактный enum
                log.warn("Abstract enum in request/response class");
                return new JsonExample(JsonConvertableUtils.stringWriter("code"));
            }
            Object[] values = clazz.getEnumConstants();
            String comment = "${description(" + clazz.getName() + ")}";

            if (values.length > 0) {
                Object example = values[0];
                return new JsonExample(JsonConvertableUtils.stringWriter(example.toString()), comment);
            } else {
                log.warn("No enum values found for class {}", clazz);
                return new JsonExample(JsonConvertableUtils.NULL_WRITER);
            }
        } else {
            return null;
        }
    }

    protected JsonExample retrievePrimitive(Class<?> clazz) {
        if (clazz == Integer.TYPE || clazz == Integer.class) {
            return new JsonExample(JsonConvertableUtils.intWriter(0));
        } else if (clazz == Long.TYPE || clazz == Long.class) {
            return new JsonExample(JsonConvertableUtils.longWriter(0L));
        } else if (clazz == Boolean.TYPE || clazz == Boolean.class) {
            return new JsonExample(JsonConvertableUtils.booleanWriter(false));
        } else if (clazz == Double.TYPE || clazz == Double.class) {
            return new JsonExample(JsonConvertableUtils.doubleWriter(0d));
        } else if (clazz == Float.TYPE || clazz == Float.class) {
            return new JsonExample(JsonConvertableUtils.floatWriter(0f));
        } else if (clazz == Short.TYPE || clazz == Short.class) {
            return new JsonExample(JsonConvertableUtils.shortWriter((short) 0));
        } else if (clazz == Byte.TYPE || clazz == Byte.class) {
            return new JsonExample(JsonConvertableUtils.byteWriter((byte) 0));
        } else {
            return null;
        }
    }

    protected String renderComments(String[] comments, String... otherComments) {
        StringBuilder res = null;
        boolean hasComments = false;
        for (String comment : comments) {
            if (comment != null) {
                if (res == null) {
                    res = new StringBuilder();
                }
                if (hasComments) {
                    res.append("; ");
                }
                res.append(comment);
                hasComments = true;
            }
        }
        for (String comment : otherComments) {
            if (comment != null) {
                if (res == null) {
                    res = new StringBuilder();
                }
                if (hasComments) {
                    res.append("; ");
                }
                res.append(comment);
                hasComments = true;
            }
        }
        if (res != null) {
            return res.toString();
        } else {
            return null;
        }
    }

    protected static class JsonExample {
        public final JsonValueWriter writer;
        public final String[] comments;

        public JsonExample(JsonValueWriter writer, String... comments) {
            this.writer = writer;
            this.comments = comments;
        }
    }

    private FullTypeInfo forceConcrete(FullTypeInfo typeInfo, RetrieveMethod retrieveMethod) {
        switch (retrieveMethod) {
            case RESPONSE_NOT_ERROR:
            case RESPONSE_ONLY_ERROR:
                return typeInfo.thisOrUpper();
            case ANY:
            default:
                return typeInfo.thisOrAnyBound();
        }
    }

    public static enum ObjectType {
        REQUEST,
        RESPONSE
    }

    private static enum RetrieveMethod {
        RESPONSE_NOT_ERROR,
        RESPONSE_ONLY_ERROR,
        ANY
    }
}
