package ru.yandex.webmaster.common.http;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import ru.yandex.kungfu.application.ApplicationInfo;
import ru.yandex.webmaster.common.WebmasterException;
import ru.yandex.webmaster.common.WebmasterExceptionSystem;
import ru.yandex.webmaster.common.WebmasterExceptionType;

/**
 * @author aherman
 */
public class ActionRouter extends AbstractHandler implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(ActionRouter.class);
    private static final Logger actionLog = LoggerFactory.getLogger("ActionResult");

    private ApplicationContext applicationContext;
    private Map<String, Action> actions = Collections.emptyMap();
    private Map<String, List<String>> prefixes = Collections.emptyMap();
    private Map<Class<? extends RequestFilter>, RequestFilter> registeredRequestFilters = Collections.emptyMap();
    private Map<Class<? extends ResponseFilter>, ResponseFilter> registeredResponseFilters = Collections.emptyMap();
    private Map<Class<?>, ParameterConverter> additionalConverters = Collections.emptyMap();

    private ApplicationInfo applicationInfo;
    private String applicationTmpFolder;

    private RequestConverter requestConverter;

    private int maxRequestFileSize = 10 * 1024 * 1024; // 10 Mb

    private static final String ENABLE_PRETTY = "_pretty";

    private static ObjectMapper JSON_OM = new ObjectMapper()
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .registerModule(new JodaModule());

    private static ObjectMapper JSON_PRETTY_OM = new ObjectMapper()
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .enable(SerializationFeature.INDENT_OUTPUT)
            .registerModule(new JodaModule());

    private static ObjectMapper XML_OM = new XmlMapper()
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .registerModule(new JodaModule());

    private static ObjectMapper XML_PRETTY_OM = new XmlMapper()
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
//            .enable(SerializationFeature.INDENT_OUTPUT)
            .registerModule(new JodaModule());

    private static final String ACTION_RESULT_LOGGING = "Action Result: status={} action={} timeMs={}";

    public void init() {
        actions = applicationContext.getBeansOfType(Action.class);
        log.info("Actions found: {}", actions.size());

        prefixes = new HashMap<>();


        List<String> allActionNames = new ArrayList<>(actions.keySet());
        Collections.sort(allActionNames);
        prefixes.put("/", allActionNames);

        for (String actionName : allActionNames) {
            String[] parts = StringUtils.split(actionName, '/');

            if (parts.length <= 1) {
                continue;
            }

            StringBuilder sb = new StringBuilder();
            sb.append('/');
            for (int i = 0; i < parts.length - 1; i++) {
                sb.append(parts[i]).append('/');
                String actionPrefix = sb.toString();
                List<String> actionsList = prefixes.get(actionPrefix);
                if (actionsList == null) {
                    actionsList = new LinkedList<>();
                    prefixes.put(actionPrefix, actionsList);
                }
                actionsList.add(actionName);
            }
        }

        log.info("Prefixes found: {}", prefixes.size());

        Set<Class<? extends RequestFilter>> allRequestFilters = new HashSet<>();
        Set<Class<? extends ResponseFilter>> allResponseFilters = new HashSet<>();

        for (Map.Entry<String, Action> actionEntry : actions.entrySet()) {
            Action<?, ?> action = actionEntry.getValue();
            List<Class<? extends RequestFilter>> actionRequestFilters = action.getRequestFilters();
            for (Class<? extends RequestFilter> filterClass : actionRequestFilters) {
                if (!RequestFilter.class.isAssignableFrom(filterClass)) {
                    throw new RuntimeException("Unsupported request filter class: action=" + actionEntry.getKey()
                            + " actionClass=" + action.getClass().getName()
                            + " filter=" + filterClass.getName());
                }
            }
            allRequestFilters.addAll(actionRequestFilters);

            List<Class<? extends ResponseFilter>> actionResponseFilters = action.getResponseFilters();
            for (Class<? extends ResponseFilter> filterClass : actionResponseFilters) {
                if (!ResponseFilter.class.isAssignableFrom(filterClass)) {
                    throw new RuntimeException("Unsupported response filter class: action=" + actionEntry.getKey()
                            + " actionClass=" + action.getClass().getName()
                            + " filter=" + filterClass.getName());
                }
            }
            allResponseFilters.addAll(actionResponseFilters);
        }

        Map<Class<? extends RequestFilter>, RequestFilter> requestFilters = new HashMap<>();
        for (Class<? extends RequestFilter> filterClass : allRequestFilters) {
            Map<String, ? extends RequestFilter> beans = applicationContext.getBeansOfType(filterClass);
            if (beans.isEmpty()) {
                throw new RuntimeException("Unable to find request filter: filter=" + filterClass.getName());
            }
            if (beans.size() > 1) {
                log.warn("Found several request filters: filter={}", filterClass.getName());
                for (Map.Entry<String, ? extends RequestFilter> entry : beans.entrySet()) {
                    log.warn("Candidate request filter: name={}, class={}", entry.getKey(), entry.getValue().getClass().getName());
                }
            }
            RequestFilter filter = beans.values().iterator().next();
            log.info("Resolved request filter: filter={} actual={}", filterClass.getName(), filter.getClass().getName());
            requestFilters.put(filterClass, filter);
        }
        registeredRequestFilters = Collections.unmodifiableMap(requestFilters);

        Map<Class<? extends ResponseFilter>, ResponseFilter> responseFilters = new HashMap<>();
        for (Class<? extends ResponseFilter> filterClass : allResponseFilters) {
            Map<String, ? extends ResponseFilter> beans = applicationContext.getBeansOfType(filterClass);
            if (beans.isEmpty()) {
                throw new RuntimeException("Unable to find response filter: filter=" + filterClass.getName());
            }
            if (beans.size() > 1) {
                log.warn("Found several response filters: filter={}", filterClass.getName());
                for (Map.Entry<String, ? extends ResponseFilter> entry : beans.entrySet()) {
                    log.warn("Candidate response filter: name={}, class={}", entry.getKey(), entry.getValue().getClass().getName());
                }
            }
            ResponseFilter filter = beans.values().iterator().next();
            log.info("Resolved response filter: filter={} actual={}", filterClass.getName(), filter.getClass().getName());
            responseFilters.put(filterClass, filter);
        }
        registeredResponseFilters = Collections.unmodifiableMap(responseFilters);

        IdentityHashMap<Class, ParameterConverter> converters = new IdentityHashMap<>();
        converters.putAll(additionalConverters);
        requestConverter = new RequestConverter(converters);
    }

    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        ResponseFormat responseFormat = getResponseFormat(target);
        if (responseFormat == ResponseFormat.UNKNOWN) {
            // Do nothing
        } else if (responseFormat == ResponseFormat.INFO) {
            baseRequest.setHandled(true);
            showInfo(target, baseRequest, request, response);
        } else {
            baseRequest.setHandled(true);
            processRequest(target, baseRequest, request, response, responseFormat);
        }
    }

    private void processRequest(String target, Request baseRequest, HttpServletRequest request,
                                HttpServletResponse response, ResponseFormat responseFormat) throws IOException {
        long startNanos = System.nanoTime();
        ActionStatus actionStatus = ActionStatus.SUCCESS;
        log.info("Start request: target={}, X-Forwarded-For: {}", target, baseRequest.getHeader("X-Forwarded-For"));

        String actionName = target.substring(0, target.length() - responseFormat.getExtension().length());
        Action<? super ActionRequest, ? extends ActionResponse> action = actions.get(actionName);
        if (action == null) {
            sendError(HttpServletResponse.SC_NOT_FOUND, target, actionName, response, responseFormat, 0L);
            return;
        }
        log.info("Request action: target={} action={} class={}", target, actionName, action.getClass().getName());
        ActionParameters actionParameters = getActionParameters(action.getClass());
        if (actionParameters == null) {
            sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, target, actionName, response,
                    responseFormat, 0L, "Unable to get request/response classes");
            return;
        }

        ActionRequest actualRequest = null;
        ActionResponse actualResponse = null;
        GenericActionResponse.ActionError actionError = null;

        try {
            actualRequest = actionParameters.requestClass.newInstance();
        } catch (Exception e) {
            actionStatus = ActionStatus.FAIL;
            String message = "Unable to instantiate request/response: " + e.getMessage();
            log.error(message, e);

            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw, true));
            actionError =
                    new GenericActionResponse.ActionError(WebmasterExceptionType.INTERNAL__UNABLE_TO_INSTANTIATE_REQUEST,
                            WebmasterExceptionSystem.INTERNAL, this.getClass().getSimpleName(), message, sw.toString());
        }

        if (actionStatus == ActionStatus.SUCCESS) {
            try {
                log.info("Request query: action={} query={}", actionName, request.getQueryString());
                baseRequest.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT,
                        new MultipartConfigElement(applicationTmpFolder,
                                maxRequestFileSize,
                                maxRequestFileSize,
                                maxRequestFileSize)
                );
                baseRequest.extractParameters();
                requestConverter.fillRequest(actualRequest, request);
                log.info("Request: action={} parameters={}", actionName, JSON_OM.writeValueAsString(actualRequest));
            } catch (WebmasterException e) {
                actionStatus = ActionStatus.FAIL;
                log.error(e.getMessage(), e);

                StringWriter sw = new StringWriter();
                e.printStackTrace(new PrintWriter(sw, true));
                actionError = new GenericActionResponse.ActionError(e.getType(), e.getType().getSystem(),
                        this.getClass().getSimpleName(), e.getMessage(), sw.toString(), e.getErrorParameters());
            } catch (Exception e) {
                log.error("Unable to read request", e);
                sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, target, actionName, response, responseFormat, 0L,
                        "Unable to read request");
                return;
            }
        }

        if (actionStatus == ActionStatus.SUCCESS) {
            try {
                List<Class<? extends RequestFilter>> requestFilters = action.getRequestFilters();
                for (Class<? extends RequestFilter> filterClass : requestFilters) {
                    RequestFilter filter = registeredRequestFilters.get(filterClass);
                    if (filter != null) {
                        filter.beforeRequest(actionName, actualRequest);
                    }
                }

                actualResponse = action.process(actualRequest);

                List<Class<? extends ResponseFilter>> responseFilters = action.getResponseFilters();
                for (Class<? extends ResponseFilter> filterClass : responseFilters) {
                    ResponseFilter filter = registeredResponseFilters.get(filterClass);
                    if (filter != null) {
                        filter.beforeResponse(actionName, ActionStatus.SUCCESS, actualRequest, actualResponse);
                    }
                }
            } catch (WebmasterException e) {
                actionStatus = ActionStatus.FAIL;
                log.error(e.getMessage(), e);

                StringWriter sw = new StringWriter();
                e.printStackTrace(new PrintWriter(sw, true));
                actionError = new GenericActionResponse.ActionError(e.getType(), e.getType().getSystem(),
                        this.getClass().getSimpleName(), e.getMessage(), sw.toString(), e.getErrorParameters());
            } catch (Exception e) {
                actionStatus = ActionStatus.FAIL;
                String message = "Unable to process request: name=" + actionName + " query=" + request.getQueryString();
                log.error(message, e);

                StringWriter sw = new StringWriter();
                e.printStackTrace(new PrintWriter(sw, true));
                actionError = new GenericActionResponse.ActionError(WebmasterExceptionType.INTERNAL__UNKNOWN,
                        WebmasterExceptionSystem.INTERNAL, this.getClass().getSimpleName(), message, sw.toString());
            }
        }

        long stopNanos = System.nanoTime();
        long durationMillis = TimeUnit.MILLISECONDS.convert(stopNanos - startNanos, TimeUnit.NANOSECONDS);
        GenericActionResponse genericActionResponse = new GenericActionResponse(
                applicationInfo.getProject() + "-" + applicationInfo.getModule(),
                applicationInfo.getVersion() + "@" + applicationInfo.getBuildDate(),
                DateTime.now(),
                actionName,
                actionStatus,
                durationMillis,
                actualRequest,
                actualResponse,
                actionError
        );

        log.info("Finish request: status={} target={} action={} httpCode={} timeMs={}", actionStatus, target, actionName,
                response.getStatus(), durationMillis);

        final String actionResult;
        if (actionError == null) {
            response.setStatus(HttpServletResponse.SC_OK);
            actionResult = "SUCCESS";
        } else {
            if (responseFormat == ResponseFormat.XML) {
                response.setStatus(HttpServletResponse.SC_OK);
            } else {
                if (actionError.getSubsystem() == WebmasterExceptionSystem.INTERNAL) {
                    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                } else {
                    response.setStatus(HttpServletResponse.SC_OK);
                }
            }
            actionResult = actionError.getSubsystem() == WebmasterExceptionSystem.INTERNAL ? "FAIL" : "SUCCESS";
        }
        actionLog.info(ACTION_RESULT_LOGGING, actionResult, actionName, durationMillis);

        boolean prettyPrint = request.getParameter(ENABLE_PRETTY) != null;

        response.setCharacterEncoding("UTF-8");
        response.setContentType(responseFormat.getMimeType());

        ObjectMapper om = JSON_OM;
        if (responseFormat == ResponseFormat.JSON) {
            if (prettyPrint || actionError != null) {
                om = JSON_PRETTY_OM;
            } else {
                om = JSON_OM;
            }

        } else if (responseFormat == ResponseFormat.XML) {
            if (prettyPrint || actionError != null) {
                om = XML_PRETTY_OM;
            } else {
                om = XML_OM;
            }
        }

        ServletOutputStream os = null;
        try {
            os = response.getOutputStream();
            om.writeValue(os, genericActionResponse);
        } finally {
            if (os != null) {
                os.flush();
            }
        }
        log.info("Response sent: target={} action={}", target, actionName);
    }

    private void showInfo(String target, Request baseRequest, HttpServletRequest request,
                          HttpServletResponse response) throws IOException {
        String actionOrPrefix = target.substring(0, target.length() - ResponseFormat.INFO.getExtension().length());
        List<String> actionsList = prefixes.get(actionOrPrefix);
        if (actionsList != null) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("text/plain");

            PrintWriter writer = response.getWriter();
            for (String action : actionsList) {
                writer.println(action);
            }
            writer.flush();
            return;
        }

        Action<?, ?> action = actions.get(actionOrPrefix);
        if (action != null) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType(ResponseFormat.INFO.getMimeType());

            PrintWriter writer = response.getWriter();

            Class<? extends Action> actionClass = action.getClass();
            writer.println("Class: " + actionClass.getName());

            writer.println("From type");
            ActionParameters actionParameters = getActionParameters(actionClass);
            if (actionParameters != null) {
                writer.println("Request: " + actionParameters.requestClass.getName());
                writer.println("Response: " + actionParameters.responseClass.getName());
            }

            List<Class<? extends RequestFilter>> requestFilters = action.getRequestFilters();
            writer.println("Request filters:");
            for (Class<? extends RequestFilter> requestFilter : requestFilters) {
                RequestFilter actualRequestFilter = registeredRequestFilters.get(requestFilter);
                if (actualRequestFilter == null) {
                    writer.println("NOT FOUND: filter=" + requestFilter.getName());
                } else {
                    writer.println("filter=" + requestFilter.getName() + " actual=" + actualRequestFilter.getClass().getName());
                }
            }

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

            writer.flush();
            return;
        }

        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
    }

    private ActionParameters getActionParameters(Class<? extends Action> actionClass) {
        Type genericSuperclass = actionClass.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            if (Action.class.isAssignableFrom((Class) parameterizedType.getRawType())) {
                Class<? extends ActionRequest> reqClass = null;
                Class<? extends ActionResponse> respClass = null;

                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    Class<?> clazz = (Class<?>) actualTypeArgument;
                    if (ActionRequest.class.isAssignableFrom(clazz)) {
                        reqClass = (Class<? extends ActionRequest>) clazz;
                    }
                    if (ActionResponse.class.isAssignableFrom(clazz)) {
                        respClass = (Class<? extends ActionResponse>) clazz;
                    }
                }
                if (reqClass != null) {
                    return new ActionParameters(reqClass, respClass);
                }
            }
        }
        return null;
    }

    private void sendError(int status, String requestTarget, String actionName, HttpServletResponse response,
                           ResponseFormat responseFormat, long durationMs) {
        log.error("Finish request: status={} target={} action={} httpCode={}", ActionStatus.FAIL, requestTarget, actionName,
                status);
        actionLog.info(ACTION_RESULT_LOGGING, "FAIL", actionName, durationMs);
        response.setStatus(status);
    }

    private void sendError(int status, String requestTarget, String actionName, HttpServletResponse response,
                           ResponseFormat responseFormat, long durationMs, String message) throws IOException {
        log.error("Finish request: status={} target={} action={} httpCode={}, message={}", ActionStatus.FAIL, requestTarget,
                actionName, status, message);

        actionLog.info(ACTION_RESULT_LOGGING, "FAIL", actionName, durationMs);

        response.setStatus(status);
        response.setContentType("text/plain");
        PrintWriter writer = response.getWriter();
        writer.println(message);
        writer.close();
    }

    protected static ResponseFormat getResponseFormat(String target) {
        int commaPosition = target.lastIndexOf('.');
        if (commaPosition < 0) {
            return ResponseFormat.UNKNOWN;
        }
        String extension = target.substring(commaPosition);
        for (ResponseFormat format : ResponseFormat.values()) {
            if (format.getExtension().equals(extension)) {
                return format;
            }
        }

        return ResponseFormat.UNKNOWN;
    }

    private static class ActionParameters {
        public final Class<? extends ActionRequest> requestClass;
        public final Class<? extends ActionResponse> responseClass;

        private ActionParameters(Class<? extends ActionRequest> requestClass, Class<? extends ActionResponse> responseClass) {
            this.requestClass = requestClass;
            this.responseClass = responseClass;
        }
    }

    private static enum ResponseFormat {
        UNKNOWN("", ""),

        INFO(".info", "text/plain"),
        XML(".xml", "application/xml"),
        JSON(".json", "application/json"),;

        private final String extension;
        private final String mimeType;

        private ResponseFormat(String extension, String mimeType) {
            this.extension = extension;
            this.mimeType = mimeType;
        }

        public String getExtension() {
            return extension;
        }

        public String getMimeType() {
            return mimeType;
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Required
    public void setApplicationInfo(ApplicationInfo applicationInfo) {
        this.applicationInfo = applicationInfo;
    }

    @Required
    public void setApplicationTmpFolder(String applicationTmpFolder) {
        this.applicationTmpFolder = applicationTmpFolder;
    }

    public void setMaxRequestFileSize(int maxRequestFileSize) {
        this.maxRequestFileSize = maxRequestFileSize;
    }

    public void setAdditionalConverters(Map<Class<?>, ParameterConverter> additionalConverters) {
        this.additionalConverters = additionalConverters;
    }
}
