package ru.yandex.qe.dispenser.ws.intercept;

import java.io.IOException;

import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import com.google.common.base.Stopwatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import ru.yandex.qe.dispenser.solomon.SolomonHolder;
import ru.yandex.qe.dispenser.ws.sensors.SensorsHolder;

public class AccessLogFilter implements Filter {

    private static final Logger LOG = LoggerFactory.getLogger(AccessLogFilter.class);

    public static final String LOGIN = "access_log.login";
    public static final String UID = "access_log.uid";
    public static final String INNER_REQ_ID = "access_log.innerRequestId";
    public static final String ENDPOINT = "access_log.endpoint";
    public static final String TVM_CLIENT_ID = "access_log.tvmClientId";
    public static final String ENDPOINT_SERVICE = "access_log.endpointService";

    private SensorsHolder sensorsHolder;

    @Override
    public void init(final FilterConfig filterConfig) {
        final ServletContext servletContext = filterConfig.getServletContext();
        final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        final SolomonHolder solomonHolder = webApplicationContext.getBean(SolomonHolder.class);
        this.sensorsHolder = new SensorsHolder(solomonHolder.getRootRegistry());
    }

    @Override
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain)
            throws IOException, ServletException {
        final Stopwatch stopwatch = Stopwatch.createStarted();
        final AccessLogEntry.EntryBuilder entryBuilder = AccessLogEntry.builder(servletRequest);
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            if (servletRequest.isAsyncStarted()) {
                final LoggingListener loggingListener = new LoggingListener(stopwatch, entryBuilder, sensorsHolder);
                servletRequest.getAsyncContext().addListener(loggingListener, servletRequest, servletResponse);
            } else {
                log(servletRequest, servletResponse, stopwatch, entryBuilder, sensorsHolder);
            }
        }
    }

    @Override
    public void destroy() {
        this.sensorsHolder = null;
    }

    private static void log(final ServletRequest servletRequest, final ServletResponse servletResponse, final Stopwatch stopwatch,
                            final AccessLogEntry.EntryBuilder entryBuilder, final SensorsHolder sensorsHolder) {
        stopwatch.stop();
        entryBuilder.addResult(servletResponse, servletRequest, stopwatch);
        final AccessLogEntry logEntry = entryBuilder.build();
        logEntry.log();
        logEntry.updateSensors(sensorsHolder);
    }

    private static final class LoggingListener implements AsyncListener {

        private final Stopwatch stopwatch;
        private final AccessLogEntry.EntryBuilder entryBuilder;
        private final SensorsHolder sensorsHolder;

        private LoggingListener(final Stopwatch stopwatch,
                                final AccessLogEntry.EntryBuilder entryBuilder,
                                final SensorsHolder sensorsHolder) {
            this.stopwatch = stopwatch;
            this.entryBuilder = entryBuilder;
            this.sensorsHolder = sensorsHolder;
        }

        @Override
        public void onComplete(final AsyncEvent asyncEvent) {
            log(asyncEvent.getSuppliedRequest(), asyncEvent.getSuppliedResponse(), stopwatch, entryBuilder, sensorsHolder);
        }

        @Override
        public void onTimeout(final AsyncEvent asyncEvent) {
            LOG.error("Async context timeout");
            log(asyncEvent.getSuppliedRequest(), asyncEvent.getSuppliedResponse(), stopwatch, entryBuilder, sensorsHolder);
        }

        @Override
        public void onError(final AsyncEvent asyncEvent) {
            LOG.error("Async context error", asyncEvent.getThrowable());
            log(asyncEvent.getSuppliedRequest(), asyncEvent.getSuppliedResponse(), stopwatch, entryBuilder, sensorsHolder);
        }

        @Override
        public void onStartAsync(final AsyncEvent asyncEvent) {
        }

    }

}
