package com.github.adminfaces.template.session;

import static com.github.adminfaces.template.util.Assert.*;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.enterprise.inject.spi.CDI;
import javax.inject.Inject;
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 javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.github.adminfaces.template.config.AdminConfig;
import com.github.adminfaces.template.util.Constants;

/**
 * Based on https://github.com/conventions/core/blob/master/src/main/java/org/conventionsframework/filter/ConventionsFilter.java
 * Created by rafael-pestano on 07/01/17.
 *
 * This filter controls when user must be redirected to logon or index page
 * and saves current url to redirect back when session expires
 */
@WebFilter(urlPatterns = {"/*"})
public class AdminFilter implements Filter {

    private static final String FACES_RESOURCES = "/javax.faces.resource";
    private static final Logger log = Logger.getLogger(AdminFilter.class.getName());

    private boolean disableFilter;
    private String loginPage;
    private String indexPage;
    private String redirectPrefix;

    @Inject
    AdminSession adminSession;

    @Inject
    AdminConfig adminConfig;

    private final List<String> ignoredResources = new ArrayList<>();

    @Override
    public void init(FilterConfig filterConfig) {
        if(adminConfig == null) {
            initBeans();
        }
        String disableAdminFilter = filterConfig.getServletContext().getInitParameter(Constants.InitialParams.DISABLE_FILTER);
        if (adminConfig.isDisableFilter() || has(disableAdminFilter) && Boolean.valueOf(disableAdminFilter)) {
            disableFilter = true;
        }
        if (!disableFilter) {
            try {
                loginPage = filterConfig.getServletContext().getInitParameter(Constants.InitialParams.LOGIN_PAGE);
                if (!has(loginPage)) {
                    loginPage = has(adminConfig) ? adminConfig.getLoginPage() : Constants.DEFAULT_LOGIN_PAGE;
                }
                String errorPage = filterConfig.getServletContext().getInitParameter(Constants.InitialParams.ERROR_PAGE);
                if (!has(errorPage)) {
                    errorPage = Constants.DEFAULT_ERROR_PAGE;
                }
                indexPage = filterConfig.getServletContext().getInitParameter(Constants.InitialParams.INDEX_PAGE);
                if (!has(indexPage)) {
                    indexPage = has(adminConfig) ? adminConfig.getIndexPage() : Constants.DEFAULT_INDEX_PAGE;
                }

                //removes leading '/'
                errorPage = errorPage.startsWith("/") ? errorPage.substring(1) : errorPage;
                loginPage = loginPage.startsWith("/") ? loginPage.substring(1) : loginPage;
                indexPage = indexPage.startsWith("/") ? indexPage.substring(1) : indexPage;

                ignoredResources.add("/" + loginPage.substring(0, loginPage.lastIndexOf(".")));//we need leading slash for ignoredResources
                ignoredResources.add("/" + errorPage.substring(0, errorPage.lastIndexOf(".")));

                String configuredResouces = adminConfig.getIgnoredResources();
                if (has(configuredResouces)) {
                    this.ignoredResources.addAll(Arrays.asList(configuredResouces.split(",")));
                    for (String ignoredResource : ignoredResources) {
                        if (!ignoredResource.startsWith("/")) { //we need leading slash for ignoredResources beucase getServletPath (in this#skipResource) returns a string with leading slash
                            ignoredResources.set(ignoredResources.indexOf(ignoredResource), "/" + ignoredResource);
                        }
                    }
                }

            } catch (Exception e) {
                log.log(Level.SEVERE, "problem initializing admin filter", e);
            }
        }

    }

    /**
     * Workaround for open web beans on tomcat, see https://stackoverflow.com/questions/45205472/apache-openwebbeanscdi-servlet-injection-not-working
     */
    private void initBeans() {
        try {
            Class.forName("javax.enterprise.inject.spi.CDI");
            adminConfig = CDI.current().select(AdminConfig.class).get();
            adminSession = CDI.current().select(AdminSession.class).get();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Could not initialize beans via lookup.", e);
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        if (disableFilter) {
            chain.doFilter(req, resp);
            return;
        }
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;


        if (request.getRequestURI().equals(request.getContextPath() + "/")
                || (adminSession.isLoggedIn() && request.getRequestURI().endsWith(loginPage))) {
            response.sendRedirect(getRedirectPrefix(request) + request.getContextPath() + "/" + indexPage);
            return;
        }

        if (request.getRequestURI().contains(request.getContextPath() + "/public/")) {
            chain.doFilter(req, resp);
            return;
        }

        if (skipResource(request, response) || adminSession.isLoggedIn()) {
            if (!adminSession.isUserRedirected() && adminSession.isLoggedIn() && has(request.getHeader("Referer")) && request.getHeader("Referer").contains("?page=")) {
                adminSession.setUserRedirected(true);
                String pageFromURL = request.getContextPath() + extractPageFromURL(request.getHeader("Referer"));
                log.info("Redirecting user back to " + pageFromURL);
                response.sendRedirect(getRedirectPrefix(request) + pageFromURL);
                return;
            }
            try {
                chain.doFilter(req, resp);
            } catch (FileNotFoundException e) {
                log.log(Level.WARNING, "File not found", e);
                response.sendError(404);
            }
        } else { //resource not skipped (e.g a page that is not logon page) AND user not logged in
            redirectToLogon(request, (HttpServletResponse) resp);
            return;
        }

    }

	private String extractPageFromURL(String referer) {
		try {
			URL url = new URL(referer);
			String[] params = url.getQuery().split("&");
			for (String param : params) {
				String[] split = param.split("=");
				if ("page".equals(split[0])) {
					return URLDecoder.decode(split[1], "UTF-8");
				}
			}
		} catch (MalformedURLException | UnsupportedEncodingException e) {
            log.log(Level.WARNING, "Could not extract page from url", e);
		}
		return indexPage;
	}

    @Override
    public void destroy() {

    }

    /**
     * skips faces-resources, index, error or logon pages
     *
     * @param request
     * @return true if resource must be skipped by the filter false otherwise
     */
    private boolean skipResource(HttpServletRequest request, HttpServletResponse response) {
        String path = request.getServletPath();
        if (path.contains(".")) {
            path = path.substring(0, path.lastIndexOf("."));
        }
        boolean skip = path.startsWith(FACES_RESOURCES) || shouldIgnoreResource(path) || response.getStatus() == HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
        return skip;
    }

    private void redirectToLogon(HttpServletRequest request, HttpServletResponse response) {
        try {
            String referer = request.getHeader("Referer");
            String recoveryUrlParams;
            //get request parameters
            if (has(referer) && referer.contains("?")) {
                recoveryUrlParams = referer.substring(referer.lastIndexOf("?") + 1);
            } else {
                recoveryUrlParams = request.getQueryString();
            }
            //saves page where user were
            String requestedPage = request.getRequestURI();
            StringBuilder recoveryUrl = null;
            if (!loginPage.equals(requestedPage) && requestedPage.contains(".")) {
                if (requestedPage.startsWith(request.getContextPath())) {
                    requestedPage = requestedPage.replaceFirst(request.getContextPath(), "");
                }
                recoveryUrl = new StringBuilder(requestedPage);
                if (has(recoveryUrlParams)) {
                    recoveryUrl.append("?").append(recoveryUrlParams);
                }
            }
            /*
             if saved page is not null and is not index page then send user to logon page and save
            / previous page in url param named 'page'
            */
            String redirectUrl = request.getContextPath() + "/" + loginPage + (has(recoveryUrl) &&
                    isValidRecoveryUrl(recoveryUrl) ? "?page=" + URLEncoder.encode(recoveryUrl.toString(), "UTF-8") : "");
            if ("partial/ajax".equals(request.getHeader("Faces-Request"))) {
                //redirect on ajax request: //http://stackoverflow.com/questions/13366936/jsf-filter-not-redirecting-after-initial-redirect
                response.setContentType("text/xml");
                response.getWriter()
                        .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
                        .printf("<partial-response><redirect url=\"%s\"></redirect></partial-response>", redirectUrl);
            } else {//normal redirect
                response.sendRedirect(getRedirectPrefix(request) + redirectUrl);
            }

        } catch (Exception e) {
            log.log(Level.SEVERE, "Could not redirect to " + loginPage, e);
        }

    }

    /**
     * Skip error pages, login and index page as recovery url because it doesn't make sense redirecting user to such pages
     *
     * @param recoveryUrl
     * @return
     */
    private boolean isValidRecoveryUrl(StringBuilder recoveryUrl) {
        String pageSuffix = adminConfig.getPageSufix();
        return !recoveryUrl.toString().contains(Constants.DEFAULT_INDEX_PAGE.replace("xhtml", pageSuffix)) && !recoveryUrl.toString().contains(Constants.DEFAULT_ACCESS_DENIED_PAGE.replace("xhtml", adminConfig.getPageSufix()))
                && !recoveryUrl.toString().contains(Constants.DEFAULT_EXPIRED_PAGE.replace("xhtml", pageSuffix)) && !recoveryUrl.toString().contains(Constants.DEFAULT_OPTIMISTIC_PAGE.replace("xhtml", adminConfig.getPageSufix()))
                && !recoveryUrl.toString().contains(Constants.DEFAULT_LOGIN_PAGE.replace("xhtml", adminConfig.getPageSufix()));
    }

    /**
     * @param path
     * @return true if requested path starts with a ignored resource (configured in admin-config.properties)
     */
    private boolean shouldIgnoreResource(String path) {
        for (String ignoredResource : ignoredResources) {
            if (path.startsWith(ignoredResource)) {
                return true;
            }
        }
        return false;
    }

    private String getRedirectPrefix(HttpServletRequest request) {
        if(redirectPrefix == null) {
            String url = request.getRequestURL().toString();

            // Find the end of the scheme in the URL.  This avoids false matches in the offset
            // calculation below.
            Matcher urlWithScheme = Pattern.compile("^(https?://).*").matcher(url);
            int urlHostIndex;
            if (urlWithScheme.matches()) {
                urlHostIndex = urlWithScheme.group(1).length();
            } else {
                urlHostIndex = 0;
            }

            String uri = request.getRequestURI();
            int offset = url.indexOf(uri, urlHostIndex);

            redirectPrefix = url.substring(0, offset);

            if(useHttps(request)) {
                log.log(Level.WARNING,"Changing request scheme to https.");
                redirectPrefix = redirectPrefix.replace("http:","https:");
            }
        }
        return redirectPrefix;
    }

    private static boolean useHttps(HttpServletRequest request) {
        String protocolProperty = System.getProperty("admin.protocol", System.getenv("admin.protocol"));

        String protoHeader = request.getHeader("X-Forwarded-Proto");
        return request.isSecure() || (protoHeader != null && protoHeader.toLowerCase().equals("https"))
            || (protocolProperty != null && protocolProperty.toLowerCase().equals("https"));
    }
}