package ru.yandex.direct.api.v5.security;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import ru.yandex.direct.api.v5.context.ApiContext;
import ru.yandex.direct.api.v5.context.ApiContextHolder;
import ru.yandex.direct.api.v5.security.exception.AccessToApiDeniedException;
import ru.yandex.direct.api.v5.security.exception.AuthenticationException;
import ru.yandex.direct.api.v5.security.exception.BadCredentialsException;
import ru.yandex.direct.api.v5.security.exception.InternalAuthenticationServiceException;
import ru.yandex.direct.api.v5.security.exception.UnknownLoginInClientLoginOrFakeLoginException;
import ru.yandex.direct.api.v5.security.exception.UnknownUserException;
import ru.yandex.direct.core.security.AccessDeniedException;
import ru.yandex.direct.tracing.util.TraceCommentVarsHolder;

/**
 * Авторизация нам нужна как можно раньше, чтобы можно было списывать баллы за невалидные запросы,
 * при этом, ошибка должна корректно заворачиваться в json/soap ответ. Поэтому на уровне фильтра мы авторизуем
 * клиента и кладём данные о успехе или исключении в apiContext, а в интерсепторе проверяем авторизацию
 * и в случае чего бросаем исключение
 */
@Component
public class AuthorizationFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(AuthorizationFilter.class);

    private final ApiContextHolder apiContextHolder;
    private final ApiAuthenticationManager authenticationManager;

    @Autowired
    public AuthorizationFilter(ApiContextHolder apiContextHolder,
                               ApiAuthenticationManager authenticationManager) {
        this.apiContextHolder = apiContextHolder;
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void init(FilterConfig filterConfig) {
        // no initialization
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ApiContext apiContext = apiContextHolder.get();

        try {
            apiContext.setPreAuthentication(authenticationManager
                    .checkApiAccessAllowed(apiContext.getAuthRequest()));
            // Сохраняем полученные данные в контексте между шагами
            apiContext.setPreAuthentication(authenticationManager
                    .checkClientLoginAccess(apiContext.getPreAuthentication()));
        } catch (UnknownLoginInClientLoginOrFakeLoginException
                | BadCredentialsException
                | AccessToApiDeniedException
                | UnknownUserException e) {
            // Неправильный ClientLogin, неизвестный юзер или отсутствие доступа к API, типовые ошибки
            logger.info(e.getMessage());
            apiContext.setAuthorizationException(e);
        } catch (AuthenticationException | AccessDeniedException e) {
            logger.error("Authorization exception caught", e);
            apiContext.setAuthorizationException(e);
        } catch (RuntimeException e) {
            logger.error("can not authorize due to unexpected exception", e);
            apiContext.setAuthorizationException(
                    new InternalAuthenticationServiceException("can not authorize due to unexpected exception", e));
        }

        try {
            DirectApiPreAuthentication preAuth = apiContext.getPreAuthentication();
            if (preAuth != null && preAuth.getOperator() != null && preAuth.getOperator().getUid() != null) {
                TraceCommentVarsHolder.get().setOperator(String.valueOf(preAuth.getOperator().getUid()));
            }
            chain.doFilter(request, response);
        } finally {
            TraceCommentVarsHolder.get().removeOperator();
        }
    }

    @Override
    public void destroy() {
        // no finalization
    }
}
