package org.gcube.data.access.storagehub.services;

import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.gcube.common.authorization.library.provider.AuthorizationProvider;
import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse;
import org.gcube.common.storagehub.model.Excludes;
import org.gcube.common.storagehub.model.NodeConstants;
import org.gcube.common.storagehub.model.acls.AccessType;
import org.gcube.common.storagehub.model.exceptions.BackendGenericError;
import org.gcube.common.storagehub.model.exceptions.InvalidCallParameters;
import org.gcube.common.storagehub.model.exceptions.InvalidItemException;
import org.gcube.common.storagehub.model.exceptions.StorageHubException;
import org.gcube.common.storagehub.model.exceptions.UserNotAuthorizedException;
import org.gcube.common.storagehub.model.items.FolderItem;
import org.gcube.common.storagehub.model.items.Item;
import org.gcube.common.storagehub.model.items.SharedFolder;
import org.gcube.common.storagehub.model.types.ItemAction;
import org.gcube.common.storagehub.model.types.NodeProperty;
import org.gcube.common.storagehub.model.types.PrimaryNodeType;
import org.gcube.data.access.storagehub.AuthorizationChecker;
import org.gcube.data.access.storagehub.Constants;
import org.gcube.data.access.storagehub.Utils;
import org.gcube.data.access.storagehub.accounting.AccountingHandler;
import org.gcube.data.access.storagehub.handlers.CredentialHandler;
import org.gcube.data.access.storagehub.handlers.Item2NodeConverter;
import org.gcube.data.access.storagehub.handlers.Node2ItemConverter;
import org.gcube.data.access.storagehub.handlers.VersionHandler;
import org.gcube.smartgears.utils.InnerMethodName;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("items")
public class ItemSharing {

	private static final Logger log = LoggerFactory.getLogger(ItemSharing.class);

	@Inject 
	RepositoryInitializer repository;

	@Inject 
	AccountingHandler accountingHandler;

	@RequestScoped
	@PathParam("id") 
	String id;

	@Context 
	ServletContext context;

	@Inject
	AuthorizationChecker authChecker;

	@Inject
	VersionHandler versionHandler;

	@Inject Node2ItemConverter node2Item;
	@Inject Item2NodeConverter item2Node;

	@PUT
	@Path("{id}/share")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.MULTIPART_FORM_DATA)
	public String share(@FormDataParam("users") Set<String> users, @FormDataParam("defaultAccessType") AccessType accessType){
		InnerMethodName.instance.set("shareFolder");
		Session ses = null;
		String toReturn = null;
		try{
			String login = AuthorizationProvider.instance.get().getClient().getId();
			ses = repository.getRepository().login(CredentialHandler.getAdminCredentials(context));
			authChecker.checkWriteAuthorizationControl(ses, id, false);

			//Item item = node2Item.getItem(ses.getNodeByIdentifier(id), Excludes.ALL);

			if (accessType==null)
				accessType = AccessType.READ_ONLY;

			if (users==null || users.isEmpty())
				throw new InvalidCallParameters("users is empty");

			Node nodeToShare = ses.getNodeByIdentifier(id);
			
			boolean alreadyShared = false;
			
			Node sharedFolderNode;
			if (!node2Item.checkNodeType(nodeToShare, SharedFolder.class))
				sharedFolderNode= shareFolder(nodeToShare, ses);
			else {
				sharedFolderNode = nodeToShare;
				alreadyShared = true;
			}
			ses.save();

			ses.getWorkspace().getLockManager().lock(sharedFolderNode.getPath(), true, true, 0,login);

			try {

				AccessControlManager acm = ses.getAccessControlManager();
				JackrabbitAccessControlList acls = AccessControlUtils.getAccessControlList(acm, sharedFolderNode.getPath());

				if (!alreadyShared) {
					Privilege[] adminPrivileges = new Privilege[] { acm.privilegeFromName(AccessType.ADMINISTRATOR.getValue()) };
					addUserToSharing(sharedFolderNode, ses, login, adminPrivileges, acls);
					users.remove(login);
				}

				Privilege[] userPrivileges = new Privilege[] { acm.privilegeFromName(accessType.getValue()) };
				for (String user : users) 
					try {
						addUserToSharing(sharedFolderNode, ses, user, userPrivileges, acls);
					}catch(Exception e){
						log.warn("error adding user {} to sharing of folder {}", user, sharedFolderNode.getName());
					}

				acm.setPolicy(sharedFolderNode.getPath(), acls);


				accountingHandler.createShareFolder(sharedFolderNode.getProperty(NodeProperty.TITLE.toString()).getString(), users, ses, sharedFolderNode, false);

				ses.save();

				toReturn =  sharedFolderNode.getIdentifier();

			} finally {
				ses.getWorkspace().getLockManager().unlock(sharedFolderNode.getPath());
			}

		}catch(RepositoryException re){
			log.error("jcr sharing", re);
			GXOutboundErrorResponse.throwException(new BackendGenericError("jcr error extracting archive", re));
		}catch(StorageHubException she ){
			log.error("error sharing", she);
			GXOutboundErrorResponse.throwException(she);
		}finally{
			if (ses!=null)
				ses.logout();
		}
		
		return toReturn;
	}

	
	
	private Node shareFolder(Node node, Session ses) throws RepositoryException, BackendGenericError, StorageHubException{
		String login = AuthorizationProvider.instance.get().getClient().getId();

		if (!node2Item.checkNodeType(node, FolderItem.class) || Utils.hasSharedChildren(node) || !node.getProperty(NodeProperty.OWNER.toString()).getString().equals(login))
			throw new InvalidItemException("item with id "+id+" cannot be shared");

		String sharedFolderName = node.getIdentifier();

		String newNodePath = Constants.SHARED_FOLDER_PATH+"/"+sharedFolderName;

		ses.move(node.getPath(),newNodePath);

		Node sharedFolderNode = ses.getNode(newNodePath);

		sharedFolderNode.setPrimaryType(PrimaryNodeType.NT_WORKSPACE_SHARED_FOLDER);

		return sharedFolderNode;
	}

	private void addUserToSharing(Node sharedFolderNode, Session ses, String user, Privilege[] userPrivileges, JackrabbitAccessControlList acls) throws RepositoryException{
		ses.getWorkspace().clone(ses.getWorkspace().getName(), sharedFolderNode.getPath(), sharedFolderNode.getProperty(NodeProperty.TITLE.toString()).getString(), false);
		String userRootWSId = ses.getNode(Utils.getHomePath(user).toPath()).getIdentifier();
		acls.addAccessControlEntry(AccessControlUtils.getPrincipal(ses, user), userPrivileges );
		Node usersNode =null; 
		if (sharedFolderNode.hasNode(NodeConstants.USERS_NAME))
			usersNode = sharedFolderNode.getNode(NodeConstants.USERS_NAME);
		else 
			usersNode = sharedFolderNode.addNode(NodeConstants.USERS_NAME);
		usersNode.setProperty(user, String.format("%s/%s",userRootWSId,sharedFolderNode.getProperty(NodeProperty.TITLE.toString()).getString()));
	}


	@PUT
	@Path("{id}/unshare")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.MULTIPART_FORM_DATA)
	public String unshare(@FormDataParam("users") Set<String> users){
		InnerMethodName.instance.set("unshareFolder");
		String login = AuthorizationProvider.instance.get().getClient().getId();
		Session ses = null;
		String toReturn = null;
		try {
			ses = repository.getRepository().login(CredentialHandler.getAdminCredentials(context));
			Node sharedNode = ses.getNodeByIdentifier(id); 
			Item item = node2Item.getItem(sharedNode, Excludes.ALL);
			if (!(item instanceof FolderItem) || !((FolderItem) item).isShared() || ((SharedFolder) item).isVreFolder())
				throw new InvalidItemException("item with id "+id+" cannot be unshared");
			SharedFolder sharedItem =(SharedFolder) item;

			Set<String> usersInSharedFolder = new HashSet<>(sharedItem.getUsers().getValues().keySet());
			usersInSharedFolder.removeAll(users);

			if (users==null || users.size()==0 || usersInSharedFolder.size()<=1)
				return unshareAll(login, ses, sharedItem);

			ses.getWorkspace().getLockManager().lock(sharedNode.getPath(), true, true, 0,login);
			try {
				if (users.size()==1 && users.contains(login))
					toReturn =  unshareCaller(login, ses, sharedItem);
				else toReturn = unsharePartial(users, login, ses, sharedItem);
			}finally {
				ses.getWorkspace().getLockManager().unlock(sharedNode.getPath());
			}
		}catch(RepositoryException re){
			log.error("jcr unsharing", re);
			GXOutboundErrorResponse.throwException(new BackendGenericError("jcr error extracting archive", re));
		}catch(StorageHubException she ){
			log.error("error unsharing", she);
			GXOutboundErrorResponse.throwException(she);
		}finally{

			if (ses!=null)
				ses.logout();
		}
		return toReturn;
	}

	
	

	private String unshareAll(String login, Session ses, SharedFolder item) throws StorageHubException, BackendGenericError, RepositoryException{

		authChecker.checkAdministratorControl(ses, item);				
		if (!login.equals(item.getOwner()))
			throw new UserNotAuthorizedException("user "+login+" not authorized to unshare all");

		Node sharedItemNode = ses.getNodeByIdentifier(item.getId());
		
		ses.getWorkspace().getLockManager().lock(sharedItemNode.getPath(), true, true, 0,login);
		Node unsharedNode;
		try {
			log.debug("user list is empty, I'm going to remove also the shared dir");
			//TODO: take the admin folder and remove his clone then move the shared folder from share to the user home and change the folder type
			String adminDirPath = (String)item.getUsers().getValues().get(login);
			String[] splitString = adminDirPath.split("/");
			String parentDirectoryId = splitString[0];
			String directoryName = splitString[1];
			Node parentNode = ses.getNodeByIdentifier(parentDirectoryId);
			log.debug("parent node path is {}/{}",parentNode.getPath(), directoryName);

			Node adminNode = ses.getNode(String.format("%s/%s",parentNode.getPath(), directoryName));
			adminNode.removeShare();

			unsharedNode = createUnsharedFolder(ses, parentNode, directoryName, item.getDescription(), login);
			
			List<Item> itemsToCopy = Utils.getItemList(sharedItemNode, Excludes.ALL, null, true, null);

			for (Item itemCopy: itemsToCopy) {
				Node itemToCopyNode = ses.getNodeByIdentifier(itemCopy.getId());
				log.debug("copying {} to {}", itemToCopyNode.getPath(), unsharedNode.getPath());
				ses.move(itemToCopyNode.getPath(), String.format("%s/%s",unsharedNode.getPath(), itemToCopyNode.getName()));
			}
			ses.save();
		}finally {
			ses.getWorkspace().getLockManager().unlock(sharedItemNode.getPath());
		}
		sharedItemNode.removeSharedSet();
		ses.save();
		log.debug("all the users have been removed, the folder is totally unshared");

		return unsharedNode.getIdentifier();

	}

	
	

	private String unshareCaller(String login, Session ses, SharedFolder item) throws StorageHubException, RepositoryException{

		if (login.equals(item.getOwner())) 
			throw new InvalidCallParameters("the callor is the owner, the folder cannot be unshared");

		if (item.getUsers().getValues().get(login)==null)
			throw new InvalidCallParameters("the folder is not shared with user "+login);

		Node sharedFolderNode =ses.getNodeByIdentifier(item.getId()); 
		
		String parentId = removeSharingForUser(login, ses, item);

		AccessControlManager acm = ses.getAccessControlManager();
		JackrabbitAccessControlList acls = AccessControlUtils.getAccessControlList(acm, sharedFolderNode.getPath());

		AccessControlEntry entryToDelete= null;
		for (AccessControlEntry ace :acls.getAccessControlEntries()) {
			if (ace.getPrincipal().getName().equals(login)) {
				entryToDelete = ace;
				break;
			}

		}
		if (entryToDelete!=null)
			acls.removeAccessControlEntry(entryToDelete);

		log.debug("removed Access control entry for user {}",login);
		Node sharedItemNode = ses.getNodeByIdentifier(item.getId());
		Node usersNode = sharedItemNode.getNode(NodeConstants.USERS_NAME);
		usersNode.remove();
		Node newUsersNode = sharedItemNode.addNode(NodeConstants.USERS_NAME);

		item.getUsers().getValues().entrySet().stream().filter(entry -> !entry.getKey().equals(login)).forEach(entry-> {try {
			newUsersNode.setProperty(entry.getKey(), (String)entry.getValue());
		} catch (Exception e) {
			log.error("error adding property to shared node users node under "+item.getId());
		}});

		acm.setPolicy(sharedFolderNode.getPath(), acls);

		ses.save();

		return parentId;

	}

	
	
	
	private String unsharePartial(Set<String> usersToUnshare, String login, Session ses, SharedFolder item) throws StorageHubException, RepositoryException {
		authChecker.checkAdministratorControl(ses, (SharedFolder)item);
		if (usersToUnshare.contains(item.getOwner()))
			throw new UserNotAuthorizedException("user "+login+" not authorized to unshare owner");

		Node sharedFolderNode =ses.getNodeByIdentifier(item.getId()); 
		
		AccessControlManager acm = ses.getAccessControlManager();
		JackrabbitAccessControlList acls = AccessControlUtils.getAccessControlList(acm, sharedFolderNode.getPath());

		for (String user : usersToUnshare) {
			removeSharingForUser(user, ses, item);

			AccessControlEntry entryToDelete= null;
			for (AccessControlEntry ace :acls.getAccessControlEntries()) {
				if (ace.getPrincipal().getName().equals(login)) {
					entryToDelete = ace;
					break;
				}

			}
			if (entryToDelete!=null)
				acls.removeAccessControlEntry(entryToDelete);
		}

		log.debug("removed Access control entry for user {}",login);
		Node sharedItemNode = ses.getNodeByIdentifier(item.getId());
		Node usersNode = sharedItemNode.getNode(NodeConstants.USERS_NAME);
		usersNode.remove();
		Node newUsersNode = sharedItemNode.addNode(NodeConstants.USERS_NAME);

		item.getUsers().getValues().entrySet().stream().filter(entry -> !usersToUnshare.contains(entry.getKey())).forEach(entry-> {try {
			newUsersNode.setProperty(entry.getKey(), (String)entry.getValue());
		} catch (Exception e) {
			log.error("error adding property to shared node users node under "+item.getId());
		}});

		acm.setPolicy(sharedFolderNode.getPath(), acls);

		ses.save();

		return item.getId();

	}


	public String removeSharingForUser(String user, Session ses, SharedFolder item) throws RepositoryException {
		String userDirPath = (String)item.getUsers().getValues().get(user);
		if (userDirPath==null) return null;
		String[] splitString = userDirPath.split("/");
		String parentDirectoryId = splitString[0];
		String directoryName = splitString[1];
		Node parentNode = ses.getNodeByIdentifier(parentDirectoryId);
		Node userNode = ses.getNode(String.format("%s/%s",parentNode.getPath(), directoryName));
		userNode.removeShare();
		accountingHandler.createUnshareFolder(directoryName, ses, parentNode, false);
		log.debug("directory removed for user {}",user);
		return parentDirectoryId;
	}


	private Node createUnsharedFolder(Session ses, Node destinationNode, String name, String description, String login) {
		FolderItem item = new FolderItem();
		Calendar now = Calendar.getInstance();
		item.setName(name);
		item.setTitle(name);
		item.setDescription(description);
		//item.setCreationTime(now);
		item.setHidden(false);
		item.setLastAction(ItemAction.CREATED);
		item.setLastModificationTime(now);
		item.setLastModifiedBy(login);
		item.setOwner(login);

		//to inherit hidden property
		//item.setHidden(destinationItem.isHidden());

		Node newNode = item2Node.getNode(ses, destinationNode, item);
		return newNode;
	}


}
