package ru.yandex.webmaster3.api.http.rest.request;

import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import ru.yandex.autodoc.common.doc.types.CollectionType;
import ru.yandex.autodoc.common.doc.types.ValueType;
import ru.yandex.webmaster3.api.http.common.request.converters.ApiSitemapIdConverter;
import ru.yandex.webmaster3.api.http.common.request.converters.ApiWebmasterHostIdConverter;
import ru.yandex.webmaster3.api.http.rest.jackson.xml.PluralNameTransformer;
import ru.yandex.webmaster3.api.http.rest.response.ApiResponse;
import ru.yandex.webmaster3.api.http.rest.response.errors.CommonApiErrors;
import ru.yandex.webmaster3.api.http.rest.routing.QueryString;
import ru.yandex.webmaster3.api.sitemap.data.ApiSitemapId;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.DateTimeParameterConverter;
import ru.yandex.webmaster3.core.http.EnumParameterConverter;
import ru.yandex.webmaster3.core.http.ParameterConverter;
import ru.yandex.webmaster3.core.http.PrimitiveParameterConverter;
import ru.yandex.webmaster3.core.http.TypeDescriptionResolver;
import ru.yandex.webmaster3.core.http.URLParameterConverter;
import ru.yandex.webmaster3.core.http.UUIDParameterConverter;
import ru.yandex.webmaster3.core.http.autodoc.FullTypeInfo;
import ru.yandex.webmaster3.core.http.internal.ActionReflectionUtils;
import ru.yandex.webmaster3.core.http.internal.ParameterInfo;
import ru.yandex.webmaster3.core.http.request.QueryIdConverter;
import ru.yandex.webmaster3.core.searchquery.QueryId;
import ru.yandex.webmaster3.core.util.ReflectionUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public class ApiRequestConverter implements TypeDescriptionResolver {
    private final IdentityHashMap<Class, ParameterConverter> converters;
    private final ParameterConverter enumParameterConverter = new EnumParameterConverter();
    private final Function<String, String> propertyNamingStrategy;

    public ApiRequestConverter(Function<String, String> propertyNamingStrategy) {
        this.propertyNamingStrategy = propertyNamingStrategy;
        this.converters = new IdentityHashMap<>();
        fillDefaultConverters();
    }

    private void fillDefaultConverters() {
        PrimitiveParameterConverter primitiveParameterConverter = new PrimitiveParameterConverter();

        addConverterIfAbsent(String.class, primitiveParameterConverter);

        addConverterIfAbsent(Boolean.class, primitiveParameterConverter);
        addConverterIfAbsent(Boolean.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Integer.class, primitiveParameterConverter);
        addConverterIfAbsent(Integer.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Long.class, primitiveParameterConverter);
        addConverterIfAbsent(Long.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Float.class, primitiveParameterConverter);
        addConverterIfAbsent(Float.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Double.class, primitiveParameterConverter);
        addConverterIfAbsent(Double.TYPE, primitiveParameterConverter);

        DateTimeParameterConverter dateTimeParameterConverter = new DateTimeParameterConverter();
        addConverterIfAbsent(Instant.class, dateTimeParameterConverter);
        addConverterIfAbsent(DateTime.class, dateTimeParameterConverter);
        addConverterIfAbsent(LocalDate.class, dateTimeParameterConverter);

        addConverterIfAbsent(UUID.class, new UUIDParameterConverter());
        addConverterIfAbsent(URL.class, new URLParameterConverter());

        addConverterIfAbsent(WebmasterHostId.class, new ApiWebmasterHostIdConverter());
        addConverterIfAbsent(ApiSitemapId.class, new ApiSitemapIdConverter());
        addConverterIfAbsent(QueryId.class, new QueryIdConverter());
    }

    private void addConverterIfAbsent(Class<?> clazz, ParameterConverter converter) {
        if (converters.containsKey(clazz)) {
            return;
        }
        converters.put(clazz, converter);
    }

    private ParameterConverter findConverter(FullTypeInfo type) {
        ParameterConverter converter = converters.get(type.getClazz());
        if (converter == null) {
            if (type.getClazz().isEnum()) {
                converter = enumParameterConverter;
            }
        }
        return converter;
    }

    public ApiResponse fillRequestParams(Map<String, List<ApiRequestParameterFilter>> filters, FullTypeInfo requestType,
                                         ApiRequest request, QueryString queryString) {
        List<ParameterInfo> parameters = getAllRequestParameters(requestType.getClazz());
        for (ParameterInfo parameter : parameters) {
            FullTypeInfo paramType = parameter.type;
            ParameterConverter converter = findConverter(paramType);
            Object convertedValue;
            String valueForFilterError;
            List<String> paramValues = queryString.getQueryParams().getOrDefault(parameter.name, Collections.emptyList());

            if (converter == null) {
                if (Iterable.class.isAssignableFrom(paramType.getClazz())) {
                    FullTypeInfo elemType = collectionElemType(paramType);
                    ParameterConverter elemConverter = findConverter(elemType);
                    if (elemConverter == null) {
                        throw new RuntimeException("Cannot find converter of type " + elemType + " for parameter " + parameter.name);
                    }
                    Collection collection = ReflectionUtils.newCollection(paramType.getClazz());
                    convertedValue = collection;
                    for (String paramValueItem : paramValues) {
                        try {
                            collection.add(elemConverter.convert(paramValueItem, elemType.getOriginalType()));
                        } catch (IllegalArgumentException e) {
                            return new CommonApiErrors.FieldValidationError(e.getMessage(), parameter.name, paramValueItem);
                        }
                    }
                    valueForFilterError = "[" + String.join(", ", paramValues) + "]";
                } else {
                    throw new RuntimeException("Cannot find converter of type " + paramType + " for parameter " + parameter.name);
                }
            } else {
                Optional<String> valueOpt = paramValues
                        .stream()
                        .findFirst();
                if (!valueOpt.isPresent() || StringUtils.isEmpty(valueOpt.get())) {
                    if (!parameter.required) {
                        continue;
                    }
                    return new CommonApiErrors.FieldValidationError("This field is required", parameter.name, null);
                }
                String value = valueOpt.get();
                valueForFilterError = value;
                try {
                    convertedValue = converter.convert(value, paramType.getOriginalType());
                } catch (IllegalArgumentException e) {
                    return new CommonApiErrors.FieldValidationError(e.getMessage(), parameter.name, value);
                }
            }
            if (filters.containsKey(parameter.name)) {
                for (ApiRequestParameterFilter filter : filters.get(parameter.name)) {
                    Optional<String> errorMsg = filter.applyFilter(parameter, convertedValue);
                    if (errorMsg.isPresent()) {
                        return new CommonApiErrors.FieldValidationError(errorMsg.get(), parameter.name, valueForFilterError);
                    }
                }
            }
            try {
                parameter.method.invoke(request, convertedValue);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException("Failed to invoke setter method of request " + requestType + " for parameter " + parameter.name, e);
            }
        }
        return null;
    }

    private FullTypeInfo collectionElemType(FullTypeInfo typeInfo) {
        return typeInfo.ancestorFullType(Iterable.class).getGenericsList().get(0);
    }

    @Override
    public ValueType describeType(Type type) {
        FullTypeInfo typeInfo = FullTypeInfo.createFromAny(type);
        Class<?> clazz = typeInfo.getClazz();
        ParameterConverter parameterConverter = converters.get(clazz);
        if (parameterConverter == null) {
            if (clazz.isEnum()) {
                parameterConverter = enumParameterConverter;
            } else if (Iterable.class.isAssignableFrom(clazz)) {
                return new CollectionType(describeType(collectionElemType(typeInfo).getOriginalType()));
            } else {
                return null;
            }
        }
        return parameterConverter.describeType(clazz);
    }

    public List<ParameterInfo> getAllRequestParameters(Class<?> clazz) {
        return ActionReflectionUtils.getAllRequestParameters(clazz, propertyNamingStrategy)
                .stream()
                .filter(p -> p.propertyType == ParameterInfo.PropertyType.GET)
                .map(p -> {
                    if (Iterable.class.isAssignableFrom(p.type.getClazz())) {
                        if (p.name.endsWith("s")) {
                            return p.withName(PluralNameTransformer.INSTANCE.transform(p.name));
                        }
                    }
                    return p;
                })
                .collect(Collectors.toList());
    }
}
