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

import org.apache.commons.lang3.tuple.Pair;
import ru.yandex.autodoc.common.doc.DocUtils;
import ru.yandex.autodoc.common.doc.ExtraInfoItem;
import ru.yandex.autodoc.common.doc.annotation.Description;
import ru.yandex.autodoc.common.doc.params.ParamDescriptor;
import ru.yandex.autodoc.common.doc.types.AnyObjectModel;
import ru.yandex.autodoc.common.doc.types.FieldDescription;
import ru.yandex.autodoc.common.doc.types.ObjectModel;
import ru.yandex.autodoc.common.doc.types.ValueType;
import ru.yandex.autodoc.common.doc.view.Markup;
import ru.yandex.autodoc.common.doc.view.handlers.ErrorsDocumentationMarkuper;
import ru.yandex.autodoc.common.doc.view.handlers.JsonFormatResolver;
import ru.yandex.autodoc.common.doc.view.handlers.MethodDocumentationMarkuper;
import ru.yandex.autodoc.common.doc.view.handlers.MultiFormatResolver;
import ru.yandex.autodoc.common.doc.view.handlers.ObjectFormatResolver;
import ru.yandex.autodoc.common.doc.view.handlers.XmlNoWrapArraysFormatResolver;
import ru.yandex.webmaster3.api.http.rest.AbstractApiAction;
import ru.yandex.webmaster3.api.http.rest.request.ApiRequestConverter;
import ru.yandex.webmaster3.api.http.rest.request.ApiRequestFilter;
import ru.yandex.webmaster3.api.http.rest.request.ApiRequestParameterFilter;
import ru.yandex.webmaster3.api.http.rest.request.meta.ApiRequestWithEntity;
import ru.yandex.webmaster3.api.http.rest.request.meta.ApiRequestWithRawContent;
import ru.yandex.webmaster3.api.http.rest.response.HttpStatus;
import ru.yandex.webmaster3.api.http.rest.response.errors.ApiErrorCode;
import ru.yandex.webmaster3.api.http.rest.response.errors.ApiErrorResponse;
import ru.yandex.webmaster3.api.http.rest.response.meta.ResponseWithoutEntity;
import ru.yandex.webmaster3.api.http.rest.routing.PathPart;
import ru.yandex.webmaster3.api.http.rest.routing.QueryParam;
import ru.yandex.webmaster3.api.http.rest.routing.Resource;
import ru.yandex.webmaster3.api.http.util.ApiReflectionUtil;
import ru.yandex.webmaster3.core.http.autodoc.FullTypeInfo;
import ru.yandex.webmaster3.core.http.internal.ParameterInfo;
import ru.yandex.webmaster3.core.util.ReflectionUtils;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public class ApiDocumentationBuilder {
    private static final String ERROR_CODE_FIELD = "error_code";

    private final MethodDocumentationMarkuper markuper;
    private final ObjectModelResolver objectModelResolver;
    private final ObjectFormatResolver objectFormatResolver;
    private final Map<AbstractApiAction, List<ApiRequestFilter>> requestFilters;
    private final Map<AbstractApiAction, Map<String, List<ApiRequestParameterFilter>>> requestParamFilters;
    private final List<Class<? extends ApiErrorResponse<?>>> commonResponses;
    private final ApiRequestConverter apiRequestConverter;

    public ApiDocumentationBuilder(
            ObjectModelResolver objectModelResolver,
            Map<AbstractApiAction, List<ApiRequestFilter>> requestFilters,
            Map<AbstractApiAction, Map<String, List<ApiRequestParameterFilter>>> requestParamFilters,
            List<Class<? extends ApiErrorResponse<?>>> commonResponses,
            ApiRequestConverter apiRequestConverter) {
        this.objectModelResolver = objectModelResolver;
        this.requestFilters = requestFilters;
        this.requestParamFilters = requestParamFilters;
        this.commonResponses = commonResponses;
        this.apiRequestConverter = apiRequestConverter;

        List<Pair<String, ObjectFormatResolver>> resolvers = new ArrayList<>();
        resolvers.add(Pair.of("JSON", JsonFormatResolver.INSTANCE));
        resolvers.add(Pair.of("XML", XmlNoWrapArraysFormatResolver.INSTANCE));
        objectFormatResolver = new MultiFormatResolver(resolvers);
        this.markuper = new MethodDocumentationMarkuper(
                new ErrorsDocumentationMarkuper(objectFormatResolver),
                objectFormatResolver
        );
    }

    public Markup buildDocumentationMarkup(Collection<Resource<?>> resources) {
        List<Markup> resourcesMarkup = new ArrayList<>();
        for (Resource<?> resource : resources) {
            String path = buildResourcePath(resource);
            List<Markup> actionsMarkup = new ArrayList<>();
            List<Pair<String, AbstractApiAction>> sortedActions = new ArrayList<>();
            for (Map.Entry<String, AbstractApiAction> entry : resource.getMethod2Action().entrySet()) {
                sortedActions.add(Pair.of(entry.getKey(), entry.getValue()));
            }
            // В пределах ресурса сортируем action'ы по http-методу
            Collections.sort(sortedActions, Comparator.comparing(Pair::getLeft));
            for (Pair<String, AbstractApiAction> pair : sortedActions) {
                String methodName = pair.getLeft() + " " + path;
                actionsMarkup.add(markupForAction(methodName, resource, pair.getRight()));
            }
            resourcesMarkup.add(new Markup.Section("resource-" + path.hashCode(), "Ресурс " + path, new Markup.Group(actionsMarkup)));
        }
        List<Markup> pageElements = new ArrayList<>();
        pageElements.addAll(resourcesMarkup);
        Markup commonErrorsSection = new Markup.Section("errors", "Общие ошибки", commonErrorsMarkup());
        pageElements.add(commonErrorsSection);
        return new Markup.Group(pageElements);
    }

    private Markup commonErrorsMarkup() {
        List<ApiResponseDescription> errorResponses = new ArrayList<>();
        for (Class<? extends ApiErrorResponse<?>> clazz : commonResponses) {
            findPolymorphicResponses(clazz, errorResponses);
        }
        Map<HttpStatus, List<ApiResponseDescription>> errorsMap = groupErrors(errorResponses);
        List<Markup> errorsMarkup = new ArrayList<>();
        fillErrors(errorsMap, errorsMarkup);
        return new Markup.Group(errorsMarkup);
    }

    private Markup markupForAction(String methodName, Resource<?> resource, AbstractApiAction<?, ?> action) {
        List<Markup> blocks = new ArrayList<>();
        String description = DocUtils.getDescriptionForObject(action);
        if (description != null) { /* Описание */
            blocks.add(new Markup.Text(description));
        }

        { /* Параметры */
            Class<?> requestClass = ApiReflectionUtil.getActionRequestType(action).getClazz();
            List<ParameterInfo> parameterInfos = apiRequestConverter.getAllRequestParameters(requestClass);
            Markup body;
            List<ParamDescriptor> paramDescriptors = new ArrayList<>();
            for (PathPart pathPart : resource.getPath()) {
                if (pathPart instanceof PathPart.Param) {
                    PathPart.Param param = (PathPart.Param) pathPart;
                    ParamDescriptor paramDescriptor = new ParamDescriptor(
                            "{" + param.getName() + "}",
                            true,
                            param.getParam().getParameterConverter().describeType(param.getParam().getParameterClass()),
                            null,
                            param.getDescription(),
                            Collections.emptyList()
                    );
                    paramDescriptors.add(paramDescriptor);
                }
            }
            for (QueryParam param : resource.getQuery()) {
                ParamDescriptor paramDescriptor = new ParamDescriptor(
                        param.getName(),
                        param.isRequired(),
                        param.getParam().getParameterConverter().describeType(param.getParam().getParameterClass()),
                        null,
                        param.getDescription(),
                        Collections.emptyList()
                );
                paramDescriptors.add(paramDescriptor);
            }
            parameterInfos
                    .stream()
                    .map(param -> {
                        List<Description> descriptionList = param.getAnnotations(Description.class);
                        String paramDescription = null;
                        if (!descriptionList.isEmpty()) {
                            paramDescription = descriptionList.get(0).value();
                        }
                        List<ExtraInfoItem> paramDescriptionExtensions = new ArrayList<>();
                        List<ApiRequestParameterFilter> paramFilters = requestParamFilters.get(action).get(param.name);
                        if (paramFilters != null) {
                            for (ApiRequestParameterFilter<?> filter : paramFilters) {
                                Optional<String> filterParamDesc = filter.describe(param);
                                if (filterParamDesc.isPresent()) {
                                    paramDescriptionExtensions.add(ExtraInfoItem.createSimple(filterParamDesc.get()));
                                }
                            }
                        }
                        ValueType paramType = apiRequestConverter.describeType(param.type.getOriginalType());
                        if (paramType == null) {
                            throw new RuntimeException("Unknown type " + param.type + " of parameter " + param.name + " in request " + requestClass);
                        }
                        return new ParamDescriptor(
                                param.name,
                                param.required,
                                paramType,
                                param.defaultValue,
                                paramDescription,
                                paramDescriptionExtensions
                        );
                    }).forEach(paramDescriptors::add);
            if (paramDescriptors.isEmpty()) {
                body = new Markup.Block(new Markup.Text("Параметров нет"));
            } else {
                body = markuper.paramDescriptionTable(paramDescriptors);
            }
            blocks.add(body);

            /* Тело запроса */
            if (ApiRequestWithRawContent.class.isAssignableFrom(requestClass)) {
                blocks.add(new Markup.Header("Тело запроса"));
                blocks.add(new Markup.Text("Содержимое запроса"));//TODO добавить entity description
            } else if (ApiRequestWithEntity.class.isAssignableFrom(requestClass)) {
                FullTypeInfo entityType = FullTypeInfo.createSimple(requestClass)
                        .ancestorFullType(ApiRequestWithEntity.class)
                        .getGenericsList().get(0);
                AnyObjectModel entityDesc = objectModelResolver.resolveRequestObjectModel(entityType);
                Markup entityMarkup = objectFormatResolver.resolve(entityDesc);
                blocks.add(new Markup.Header("Тело запроса"));
                blocks.add(entityMarkup);
            }
        }

        { /* Формат ответа */
            blocks.add(new Markup.Header("Ответы"));
            List<ApiResponseDescription> errorResponses = new ArrayList<>();
            ApiResponseDescription normalResponse = findPolymorphicResponses(
                    ApiReflectionUtil.getActionResponseType(action).getClazz(),
                    errorResponses
            );
            for (ApiRequestFilter requestFilter : requestFilters.get(action)) {
                findPolymorphicResponses(
                        ApiReflectionUtil.getRequestFilterResponseType(requestFilter).getClazz(),
                        errorResponses
                );
            }

            {
                Markup responseMarkup;
                if (ResponseWithoutEntity.class.isAssignableFrom(normalResponse.getResponseClass())) {
                    responseMarkup = new Markup.Text("Тело ответа не передается");
                } else {
                    FullTypeInfo responseType = FullTypeInfo.createSimple(normalResponse.getResponseClass());
                    AnyObjectModel responseModel = objectModelResolver.resolveResponseObjectModel(responseType);
                    responseMarkup = objectFormatResolver.resolve(responseModel);
                }
                blocks.add(new Markup.Section(
                        "normal-response",
                        "Нормальный ответ - код " + normalResponse.getHttpStatus().getCode(),
                        responseMarkup
                ));
            }

            Map<HttpStatus, List<ApiResponseDescription>> errorResponsesMap = groupErrors(errorResponses);
            if (errorResponsesMap.containsKey(normalResponse.getHttpStatus())) {
                throw new RuntimeException("For action " + action.getClass() +
                        " - have both error and normal responses for status code " +
                        normalResponse.getHttpStatus().getCode());
            }
            fillErrors(errorResponsesMap, blocks);
        }

        return new Markup.Section("method-" + methodName.hashCode(), "Метод " + methodName, new Markup.Group(blocks));
    }

    private void fillErrors(Map<HttpStatus, List<ApiResponseDescription>> errors, List<Markup> markups) {
        for (Map.Entry<HttpStatus, List<ApiResponseDescription>> entry : errors.entrySet()) {
            HttpStatus httpStatus = entry.getKey();
            List<ApiResponseDescription> filteredDuplicates = new ArrayList<>();
            Set<ApiErrorCode> usedCodes = new HashSet<>();
            for (ApiResponseDescription response : entry.getValue()) {
                if (usedCodes.add(response.getErrorCode())) {
                    filteredDuplicates.add(response);
                }
            }

            if (!filteredDuplicates.isEmpty()) {
                Markup errorsForStatusSection;
                if (filteredDuplicates.size() == 1) {
                    ApiResponseDescription response = filteredDuplicates.get(0);
                    errorsForStatusSection = errorResponseMarkup(response.getResponseClass(), response.getErrorCode());
                } else {
                    Collections.sort(filteredDuplicates, Comparator.comparing(d -> d.getErrorCode().toString()));
                    List<Markup> errorsForStatusList = new ArrayList<>();
                    for (ApiResponseDescription response : filteredDuplicates) {
                        Markup responseMarkup = errorResponseMarkup(response.getResponseClass(), response.getErrorCode());
                        Markup responseSection = new Markup.Section(
                                response.getErrorCode().toString(),
                                "Код ошибки " + response.getErrorCode(),
                                responseMarkup
                        );
                        errorsForStatusList.add(responseSection);
                    }
                    errorsForStatusSection = new Markup.Group(errorsForStatusList);
                }
                markups.add(new Markup.Section("error-" + httpStatus.getCode(), httpStatus.getCode() + " " + httpStatus.getDescription(), errorsForStatusSection));
            }
        }
    }

    private Map<HttpStatus, List<ApiResponseDescription>> groupErrors(List<ApiResponseDescription> errors) {
        return errors.stream()
                .collect(
                        Collectors.groupingBy(
                                ApiResponseDescription::getHttpStatus,
                                () -> new TreeMap<>(Comparator.comparing(HttpStatus::getCode)),
                                Collectors.toList()
                        )
                );
    }

    private Markup errorResponseMarkup(Class responseClass, ApiErrorCode errorCode) {
        ObjectModel model = objectModelResolver.resolveSimpleModel(FullTypeInfo.createSimple(responseClass));
        List<FieldDescription> fields = new ArrayList<>();
        fields.add(
                new FieldDescription(
                        true,
                        new ValueType.Primitive("errorCode", ValueType.STRING, errorCode.toString()),
                        null,
                        ERROR_CODE_FIELD,
                        ERROR_CODE_FIELD
                )
        );
        fields.addAll(model.getFields());
        model = new ObjectModel(
                model.getName(),
                fields,
                model.getDescription()
        );
        return objectFormatResolver.resolve(model);
    }

    private ApiResponseDescription findPolymorphicResponses(Class responseClass, List<ApiResponseDescription> acc) {
        int modifiers = responseClass.getModifiers();

        if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers)) {
            ApiResponseDescription normalResponse = null;
            for (Class innerClass : responseClass.getClasses()) {
                modifiers = innerClass.getModifiers();
                if (!Modifier.isAbstract(modifiers) &&
                        !Modifier.isInterface(modifiers) &&
                        Modifier.isStatic(modifiers) &&
                        responseClass.isAssignableFrom(innerClass)) {

                    String description = DocUtils.getDescriptionForAnnotatedElement(innerClass);
                    HttpStatus status = ApiReflectionUtil.getResponseStatus(innerClass);

                    if (ApiErrorResponse.class.isAssignableFrom(innerClass)) {
                        ApiErrorResponse<?> responseInst = (ApiErrorResponse<?>) ReflectionUtils.instantiateWithDefaults(innerClass);
                        ApiErrorCode errorCode = responseInst.getErrorCode();
                        acc.add(new ApiResponseDescription(status, errorCode, innerClass, description));
                    } else if (normalResponse != null) {
                        throw new RuntimeException("More than one normal (non-error) response found in response class " + responseClass);
                    } else {
                        normalResponse = new ApiResponseDescription(status, null, innerClass, description);
                    }
                }
            }
            return normalResponse;
        } else {
            String description = DocUtils.getDescriptionForAnnotatedElement(responseClass);
            HttpStatus status = ApiReflectionUtil.getResponseStatus(responseClass);
            return new ApiResponseDescription(status, null, responseClass, description);
        }
    }

    private static String buildResourcePath(Resource<?> resource) {
        StringBuilder result = new StringBuilder("/");
        for (PathPart<?> pathPart : resource.getPath()) {
            if (pathPart instanceof PathPart.Const) {
                result.append(((PathPart.Const) pathPart).getSegment());
            } else if (pathPart instanceof PathPart.Param) {
                result
                        .append("{")
                        .append(((PathPart.Param) pathPart).getName())
                        .append("}");
            } else {
                throw new RuntimeException("Unknown path part type " + pathPart.getClass());
            }
            result.append("/");
        }
        return result.toString();
    }

    private static class ApiResponseDescription {
        private final HttpStatus httpStatus;
        private final ApiErrorCode errorCode;
        private final Class responseClass;
        private final String description;

        public ApiResponseDescription(HttpStatus httpStatus, ApiErrorCode errorCode, Class responseClass, String description) {
            this.httpStatus = httpStatus;
            this.errorCode = errorCode;
            this.responseClass = responseClass;
            this.description = description;
        }

        public HttpStatus getHttpStatus() {
            return httpStatus;
        }

        public ApiErrorCode getErrorCode() {
            return errorCode;
        }

        public String getDescription() {
            return description;
        }

        public Class getResponseClass() {
            return responseClass;
        }
    }
}
