package it.mice.voila.runtime.web.menu;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import it.mice.voila.runtime.security.AbstractFilterSecurityInterceptorPostProcessor;
import it.mice.voila.runtime.security.ApplicationHolder;
import it.mice.voila.runtime.security.RenderSecurityBean;
import it.mice.voila.runtime.security.SecurityBean;
import it.mice.voila.runtime.web.bean.MenuDefinition;
import it.mice.voila.runtime.web.bean.MenuItemDefinition;

public class StandardMenuFilterManager implements MenuFilterManager {
    private static final Log logger = LogFactory.getLog(StandardMenuFilterManager.class);

    private MenuDefinitionFactory menuDefinitionFactory;
    private AbstractFilterSecurityInterceptorPostProcessor abstractFilterSecurityInterceptorPostProcessor;
    
    /**
     * Application holder used to retrieve current application ID
     */
    @Autowired
    ApplicationHolder applicationHolder;

    //Default build menu method. Possible values are:
    //1) disable - to disable the link;
    //2) hide - remove the menu item;
    //3) restrict - keep enabled the menu item and add a restricted zone icon;
    //4) disableAndRestrict - disable the element and draw the restricted icon.
    private String buildMenuMethod = "hide";

	public String getBuildMenuMethod() {
		return buildMenuMethod;
	}

	public void setBuildMenuMethod(String buildMenuMethod) {
		this.buildMenuMethod = buildMenuMethod;
	}

	public MenuDefinitionFactory getMenuDefinitionFactory() {
		return menuDefinitionFactory;
	}

	public void setMenuDefinitionFactory(MenuDefinitionFactory menuDefinitionFactory) {
		this.menuDefinitionFactory = menuDefinitionFactory;
	}

	public AbstractFilterSecurityInterceptorPostProcessor getFilterSecurityInterceptorPostProcessor() {
		return abstractFilterSecurityInterceptorPostProcessor;
	}

	public void setFilterSecurityInterceptorPostProcessor(
			AbstractFilterSecurityInterceptorPostProcessor abstractFilterSecurityInterceptorPostProcessor) {
		this.abstractFilterSecurityInterceptorPostProcessor = abstractFilterSecurityInterceptorPostProcessor;
	}

	public FilterSecurityInterceptor getFilterSecurityInterceptor() {
		return getFilterSecurityInterceptorPostProcessor().getFilterSecurityInterceptor();
	}

	public MenuDefinition getMenu(String menuName) {
        return getMenuDefinitionFactory().getMenu(menuName);
	}

	public MenuDefinition getFilteredMenu(String menuName) {
        MenuDefinition menuDefinition = getMenuDefinitionFactory().getMenu(menuName);
        try {
			return getFilterMenuDefinition(menuDefinition);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
    private MenuDefinition getFilterMenuDefinition(MenuDefinition menuDefinition)
			throws IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
        MenuDefinition filteredMenuDefinition = new MenuDefinition();
        PropertyUtils.copyProperties(filteredMenuDefinition, menuDefinition);
        filteredMenuDefinition.setMenuItems(new MenuItemDefinition[]{});
        ArrayList filteredMenuItems = new ArrayList();
        MenuItemDefinition[] menuItemDefinitions = menuDefinition.getMenuItems();
        for (int i = 0; i < menuItemDefinitions.length; i++) {
            if (menuItemDefinitions[i].getMenuToShow() == null) {
                //Menu item refer to an action call...
                //Check for security permission agains current authentication object.
                if (disableMenuItem(menuItemDefinitions[i])) {
                    //menu item disabled to current user
                    if (!getBuildMenuMethod(menuItemDefinitions[i]).equals("hide")) {
                        //build method to disabled, create the new menu item and force disabled to true.
                        MenuItemDefinition newMenuItemDefinition = duplicateMenuItem(menuItemDefinitions[i]);
                        if (getBuildMenuMethod(menuItemDefinitions[i]).equals("disable") || getBuildMenuMethod(menuItemDefinitions[i]).equals("disableAndRestrict")) {
                            newMenuItemDefinition.setFlagDisabled(true);
                        }
                        if (getBuildMenuMethod(menuItemDefinitions[i]).equals("restrict") || getBuildMenuMethod(menuItemDefinitions[i]).equals("disableAndRestrict")) {
                            newMenuItemDefinition.setFlagRestricted(true);
                        }
                        filteredMenuItems.add(newMenuItemDefinition);
                    }
                    //operation not allowed and build method is hidden, so do not report menu item in the output.
                } else {
                    //user authorized to execute operation.
                    filteredMenuItems.add(menuItemDefinitions[i]);
                }
            } else {
                //Menu item refer to a sub menu...
                MenuDefinition subMenuDefinition = getFilterMenuDefinition(menuItemDefinitions[i].getMenuToShow());
                if (subMenuDefinition.getMenuItems().length > 0) {
                    //sub menu contain at least one item, so attach returned menu definition to the item and continue.
                    MenuItemDefinition newMenuItemDefinition = duplicateMenuItem(menuItemDefinitions[i]);
                    newMenuItemDefinition.setMenuToShow(subMenuDefinition);
                    filteredMenuItems.add(newMenuItemDefinition);
                }
                //sub menu definition empty, so in any case remove parent menu item.
            }
        }
        if (filteredMenuItems.size() > 0) {
            filteredMenuDefinition.setMenuItems((MenuItemDefinition[]) filteredMenuItems.toArray(new MenuItemDefinition[]{}));
        }
        return filteredMenuDefinition;
    }
    
    public boolean disableMenuItem(MenuItemDefinition mid) {
    	String prefix = "";
    	if (mid.getAppId() != null && !mid.getAppId().equals(applicationHolder.getApplicationId())) {
    		prefix = mid.getAppId() + ":";
    	}
    	return disableMenuItem(prefix, mid.getLink());
    }
    
    public boolean disableMenuItem(String link) {
    	return disableMenuItem("", link);
    }
    
    public boolean disableMenuItem(String prefix, String link) {
    	return disableMenuItem(new RenderSecurityBean(prefix + link, buildSessionSecurityBean()));
    }
    
	private Map<String, Object> buildSessionSecurityBean() {
		ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
		HttpSession session = attr.getRequest().getSession();
		Enumeration<String> en = session.getAttributeNames();
		Map<String, Object> ctx = new HashMap<String, Object>();
		while (en.hasMoreElements()) {
			String key = (String) en.nextElement();
			Object value = session.getAttribute(key);
			ctx.put(key, value);
		}
		return ctx;
	}

    public boolean disableMenuItem(SecurityBean bean) {
    	DefaultFilterInvocationSecurityMetadataSource metadataSource = (DefaultFilterInvocationSecurityMetadataSource) getFilterSecurityInterceptor()
				.obtainSecurityMetadataSource();
    	Collection<ConfigAttribute> attr = metadataSource.getAttributes(new FilterInvocation(bean.getLink(), null));

        if (attr != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Secure object: " + bean.getLink() + "; ConfigAttributes: " + attr.toString());
            }

            // We check for just the property we're interested in (we do
            // not call Context.validate() like the ContextInterceptor)
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                logger.error("An Authentication object was not found in the SecurityContext. Menu item disabled.");
                return true;
            }

            // Attempt authentication if not already authenticated, or user always wants reauthentication
            Authentication authenticated;

            if (!SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
                try {
                    authenticated = getFilterSecurityInterceptor().getAuthenticationManager().authenticate(SecurityContextHolder.getContext()
                            .getAuthentication());
                } catch (AuthenticationException authenticationException) {
                    throw authenticationException;
                }

                // We don't authenticated.setAuthentication(true), because each provider should do that
                if (logger.isDebugEnabled()) {
                    logger.debug("Successfully Authenticated: " + authenticated.toString());
                }

                SecurityContextHolder.getContext().setAuthentication(authenticated);
            } else {
                authenticated = SecurityContextHolder.getContext().getAuthentication();

                if (logger.isDebugEnabled()) {
                    logger.debug("Previously Authenticated: " + authenticated.toString());
                }
            }

            // Attempt authorization
            try {
                this.getFilterSecurityInterceptor().getAccessDecisionManager().decide(authenticated, bean, attr);
            } catch (AccessDeniedException accessDeniedException) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Operation name: " + bean.getLink() + " not allowed for current user: " + authenticated.toString() + ". Menu item disabled.");
                }
                return true;
            }

            if (logger.isDebugEnabled()) {
                logger.debug("Authorization successful for operation name: " + bean.getLink() + ". Menu item enabled.");
            }
            return false;

        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Operation name: " + bean.getLink() + " refer to a public object - authentication not attempted. Menu item enabled.");
            }
            return false;
        }
    }

    public String getBuildMenuMethod(MenuItemDefinition menuItemDefinition) {
    	if (menuItemDefinition.getBuildMenuMethod() != null) {
    		return menuItemDefinition.getBuildMenuMethod();
    	}
        return buildMenuMethod;
    }

    private MenuItemDefinition duplicateMenuItem(MenuItemDefinition menuItemDefinition) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        MenuItemDefinition filteredMenuItemDefinition = new MenuItemDefinition();
        PropertyUtils.copyProperties(filteredMenuItemDefinition, menuItemDefinition);
        return filteredMenuItemDefinition;
    }
}
