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

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.yandex.autodoc.common.doc.DocUtils;
import ru.yandex.autodoc.common.doc.MethodDocumentationImpl;
import ru.yandex.autodoc.common.doc.abstracts.MethodDocumentation;
import ru.yandex.autodoc.common.doc.error.ErrorDescription;
import ru.yandex.autodoc.common.doc.error.ErrorsBlockInfo;
import ru.yandex.autodoc.common.doc.params.ParamDescriptor;
import ru.yandex.autodoc.common.doc.result.MethodResult;
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.ObjectFormatResolver;
import ru.yandex.webmaster3.core.http.Action;
import ru.yandex.webmaster3.core.http.ActionEffect;
import ru.yandex.webmaster3.core.http.ActionRequest;
import ru.yandex.webmaster3.core.http.ActionResponse;
import ru.yandex.webmaster3.core.http.BinaryActionRequest;
import ru.yandex.webmaster3.core.http.RequestConverter;
import ru.yandex.webmaster3.core.http.RequestFilter;
import ru.yandex.webmaster3.core.http.RequestTimeout;
import ru.yandex.webmaster3.core.http.ResponseFilter;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.http.internal.ActionParameters;
import ru.yandex.webmaster3.core.http.internal.ActionReflectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public class ActionDocumentationProvider {
    private static final Logger log = LoggerFactory.getLogger(ActionDocumentationProvider.class);

    private static final ObjectFormatResolver OBJECT_FORMAT = JsonFormatResolver.INSTANCE;
    private static final ErrorsDocumentationMarkuper ERRORS_DOCUMENTATION_MARKUPER =
            new ErrorsDocumentationMarkuper(OBJECT_FORMAT);
    private static final MethodDocumentationMarkuper METHOD_DOCUMENTATION_MARKUPER =
            new MethodDocumentationMarkuper(ERRORS_DOCUMENTATION_MARKUPER, OBJECT_FORMAT);

    private final RequestConverter requestConverter;
    //    private final ObjectMapper responseOM;
    private final ClassShapeRetriever responseShapeRetriever;

    public ActionDocumentationProvider(RequestConverter requestConverter, ObjectMapper responseOM) {
        this.requestConverter = requestConverter;
//        this.responseOM = responseOM;
        responseShapeRetriever = new ClassShapeRetriever(responseOM, ClassShapeRetriever.ObjectType.RESPONSE);
    }

    private List<ParamDescriptor> getRequestParameters(Class<? extends ActionRequest> reqClass) {
        return requestConverter.describeParameters(reqClass);
    }

    private MethodResult getMethodResult(Class<? extends ActionResponse> resClass) {
        return MethodResult.createJsonValueResult(responseShapeRetriever.retrieve(FullTypeInfo.createSimple(resClass)));
    }

    private List<WebmasterErrorDescription> getMethodErrors(Class<? extends ActionResponse> resClass) {
        return responseShapeRetriever.retrieveErrors(resClass);
    }

    private List<WebmasterErrorDescription> getRequestFilterErrors(Collection<RequestFilter> filters) {
        List<WebmasterErrorDescription> result = new ArrayList<>();
        for (RequestFilter filter : filters) {
            result.addAll(responseShapeRetriever.retrieveErrors(ActionReflectionUtils.getRequestFilterResponseType(filter)));
        }
        return result;
    }

    private static String getCategory(Class<? extends Action> actionClass) {
        String result = DocUtils.getCategoryForAnnotatedElement(actionClass);
        if (result == null) {
            result = DocUtils.getCategoryForAnnotatedElement(actionClass.getPackage());
        }
        return result;
    }

    private List<ErrorsBlockInfo> getCommonErrorsDocumentation() {
        List<WebmasterErrorDescription> allErrors = getMethodErrors(WebmasterErrorResponse.class);
        Map<Enum<?>, List<ErrorDescription>> subsystem2Errors = new HashMap<>();

        for (WebmasterErrorDescription description : allErrors) {
            List<ErrorDescription> forSubsystemList = subsystem2Errors.get(description.getSubsystem());
            if (forSubsystemList == null) {
                forSubsystemList = new ArrayList<>();
                subsystem2Errors.put(description.getSubsystem(), forSubsystemList);
            }
            forSubsystemList.add(description);
        }
        List<Enum<?>> subsystems = new ArrayList<>(subsystem2Errors.keySet());
        Collections.sort(subsystems, Comparator.comparing(e -> e.name()));
        List<ErrorsBlockInfo> result = new ArrayList<>();
        for (Enum<?> subsystem : subsystems) {
            result.add(new ErrorsBlockInfo(subsystem.name() + " errors", null, subsystem2Errors.get(subsystem)));
        }
        return result;
    }

    public Markup makeDocumentationMarkup(String name, Action action,
                                          Collection<RequestFilter> requestFilters,
                                          Map<Class<? extends ResponseFilter>, ResponseFilter> registeredResponseFilters) {
        Class<? extends Action> actionClass = action.getClass();
        ActionParameters actionParameters = ActionReflectionUtils.getActionParameters(actionClass);

        List<ParamDescriptor> parameters = getRequestParameters(actionParameters.requestClass);
        MethodResult methodResult = getMethodResult(actionParameters.responseClass);
        List<ErrorDescription> methodErrors = new ArrayList<>(getMethodErrors(actionParameters.responseClass));
        methodErrors.addAll(getRequestFilterErrors(requestFilters));
        Collections.sort(methodErrors, Comparator.comparing(desc -> desc.problem));

        String textParametersDescription = null;
        if (BinaryActionRequest.class.isAssignableFrom(actionParameters.requestClass)) {
            String desc = DocUtils.getDescriptionForAnnotatedElement(actionParameters.requestClass);
            textParametersDescription = "HTTP body as file" + (desc == null ? "" : ": " + desc);
        }
        String description = DocUtils.getDescriptionForObject(action);
        String category = getCategory(actionClass);

        boolean deprecated = actionClass.isAnnotationPresent(Deprecated.class);

        StringBuilder additionalDescription = new StringBuilder();

        additionalDescription.append("Class: " + actionClass.getName() + "\n");

        additionalDescription.append("From type" + "\n");
        additionalDescription.append("Request: " + actionParameters.requestClass.getName() + "\n");
        additionalDescription.append("Response: " + actionParameters.responseClass.getName() + "\n");

        List<String> methodExtraDescription = new ArrayList<>();
        ActionEffect effect = ActionReflectionUtils.getActionEffect(actionClass);
        if (effect != null) {
            String effectString;
            switch (effect) {
                case READ:
                    effectString = "Не мутирующая";
                    break;
                case WRITE:
                    effectString = "Мутирующая";
                    break;
                default:
                    throw new RuntimeException("Unkwnown action effect " + effect);
            }
            methodExtraDescription.add(effectString);
        }
        for (RequestFilter requestFilter : requestFilters) {
            String extraDesc = AutodocUtil.getClassDescription(requestFilter.getClass());
            if (extraDesc != null) {
                methodExtraDescription.add(extraDesc);
            }
            additionalDescription.append("filter=" + requestFilter.getClass().getName() + "\n");
            parameters = requestFilter.clarifyParameters(parameters);
        }
        Collections.sort(parameters,
                Comparator.comparing(ParamDescriptor::isRequired).reversed()
                        .thenComparing(ParamDescriptor::getName)
        );

        List<Class<? extends ResponseFilter>> responseFilters = action.getResponseFilters();
        additionalDescription.append("Response filters:" + "\n");
        for (Class<? extends ResponseFilter> responseFilter : responseFilters) {
            ResponseFilter actualResponseFilter = registeredResponseFilters.get(responseFilter);
            if (actualResponseFilter == null) {
                additionalDescription.append("NOT FOUND: filter=" + responseFilter.getName() + "\n");
            } else {
                additionalDescription.append("filter=" + responseFilter.getName() + " actual=" + actualResponseFilter.getClass().getName() + "\n");
            }
        }

        MethodDocumentation methodDocumentation = new MethodDocumentationImpl(name, deprecated, false, null,
                methodErrors, methodResult, parameters, textParametersDescription, description, category, Collections.emptyMap());
        List<Markup> markupItems = new ArrayList<>();
        // Описание
        if (!StringUtils.isEmpty(description)) {
            markupItems.add(new Markup.Section("desc", "Описание", new Markup.Text(description)));
        }

        //Свойства action'а
        if (!methodExtraDescription.isEmpty()) {
            markupItems.add(new Markup.Section("props", "Свойства", new Markup.UL(
                    methodExtraDescription
                            .stream()
                            .map(s -> new Markup.I(new Markup.Text(s)))
                            .collect(Collectors.toList())
            )));
        }
        // Время выполнения
        if (actionClass.isAnnotationPresent(RequestTimeout.class)) {
            RequestTimeout requestTimeout = actionClass.getAnnotation(RequestTimeout.class);
            markupItems.add(new Markup.Section("timings", "Таймаут выполнения",
                    new Markup.PreformattedText(String.valueOf(requestTimeout.value()))));
        }
        // Параметры
        markupItems.add(METHOD_DOCUMENTATION_MARKUPER.methodParams(methodDocumentation));
        // Ответ
        markupItems.add(METHOD_DOCUMENTATION_MARKUPER.methodOutput(methodDocumentation));
        // Ошибки
        markupItems.add(METHOD_DOCUMENTATION_MARKUPER.methodErrors(methodDocumentation));
        // Общие ошибки
        markupItems.add(new Markup.Section("common-errors", "Общие ошибки",
                ERRORS_DOCUMENTATION_MARKUPER.toMarkup(getCommonErrorsDocumentation())));
        // Дополнительная информация
        markupItems.add(new Markup.Section("extra-info", "Дополнительная информация",
                new Markup.PreformattedText(additionalDescription.toString())));
        return new Markup.Group(markupItems);
    }
}
