/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package com.jaspersoft.jasperserver.api.metadata.tenant.service.impl;


import java.util.*;

import com.jaspersoft.jasperserver.api.metadata.common.service.impl.hibernate.util.IlikeEscapeAwareExpression;
import com.jaspersoft.jasperserver.api.metadata.common.util.DatabaseCharactersEscapeResolver;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Role;
import com.jaspersoft.jasperserver.api.metadata.user.domain.User;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.criterion.*;

import com.jaspersoft.jasperserver.api.JSException;
import com.jaspersoft.jasperserver.api.common.domain.ExecutionContext;
import com.jaspersoft.jasperserver.api.metadata.common.service.ResourceFactory;
import com.jaspersoft.jasperserver.api.metadata.common.service.impl.HibernateDaoImpl;
import com.jaspersoft.jasperserver.api.metadata.common.service.impl.hibernate.util.RepositoryUtils;
import com.jaspersoft.jasperserver.api.metadata.tenant.service.TenantPersistenceResolver;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Tenant;
import com.jaspersoft.jasperserver.api.metadata.user.domain.impl.hibernate.RepoTenant;
import com.jaspersoft.jasperserver.api.metadata.user.service.TenantService;

/**
 * Tenant service implementation.
 *
 * @author achan
 */
public class TenantServiceImpl extends HibernateDaoImpl 
		implements TenantService, TenantPersistenceResolver {

	protected static final Log log = LogFactory.getLog(TenantServiceImpl.class);
	private ResourceFactory objectMappingFactory;
	private ResourceFactory persistentClassFactory;
	private String userOrgIdDelimiter;
    private DatabaseCharactersEscapeResolver databaseCharactersEscapeResolver;

    public void setDatabaseCharactersEscapeResolver(DatabaseCharactersEscapeResolver databaseCharactersEscapeResolver) {
        this.databaseCharactersEscapeResolver = databaseCharactersEscapeResolver;
    }
	
	public ResourceFactory getPersistentClassFactory() {
		return persistentClassFactory;
	}
	
	public void setPersistentClassFactory(ResourceFactory persistentClassFactory) {
		this.persistentClassFactory = persistentClassFactory;
	}
	
	public ResourceFactory getObjectMappingFactory() {
		return objectMappingFactory;
	}

	public void setObjectMappingFactory(ResourceFactory objectFactory) {
		this.objectMappingFactory = objectFactory;
	}
	
	protected RepoTenant getRepoTenant(String tenantId, boolean required) {
		DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());
		criteria.add(Restrictions.naturalId().set("tenantId", tenantId));
		List tenantList = getHibernateTemplate().findByCriteria(criteria);
		RepoTenant tenant;		
		if (tenantList.isEmpty()) {
			if (required) {
				throw new JSException("Tenant not found with Tenant ID \"" + tenantId + "\"");//TODO i18n
			}
			
			log.debug("Tenant not found with Tenant ID \"" + tenantId + "\"");
			tenant = null;
		} else {
			tenant = (RepoTenant) tenantList.get(0);
		}
		return tenant;
	}

    protected RepoTenant getRepoTenantByAlias(String tenantAlias, boolean required) {
        DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());
        criteria.add(Restrictions.eq("tenantAlias", tenantAlias));
        List tenantList = getHibernateTemplate().findByCriteria(criteria);
        RepoTenant tenant;
        if (tenantList.isEmpty()) {
            if (required) {
                throw new JSException("Tenant not found with Tenant Alias \"" + tenantAlias + "\"");//TODO i18n
            }

            log.debug("Tenant not found with Tenant Alias \"" + tenantAlias + "\"");
            tenant = null;
        } else {
            tenant = (RepoTenant) tenantList.get(0);
        }
        return tenant;
    }

	protected Class persistentTenantClass() {
		return getPersistentClassFactory().getImplementationClass(Tenant.class);
	}

	
	protected RepoTenant getRepoTenantBasedOnTenantUri(ExecutionContext context, String tenantUri) {
		DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());
		criteria.add(Restrictions.eq("tenantUri", tenantUri));
		List tenantList = getHibernateTemplate().findByCriteria(criteria);
		RepoTenant tenant = null;		
		if (tenantList.isEmpty()) {
			log.debug("Tenant not found with Tenant Name \"" + tenantUri + "\"");
		} else {
			tenant = (RepoTenant) tenantList.get(0);
		}
		return tenant;
	}	

	/* (non-Javadoc)
	 * @see com.jaspersoft.jasperserver.api.metadata.user.service.UserAuthorityService#putUser(com.jaspersoft.jasperserver.api.common.domain.ExecutionContext, com.jaspersoft.jasperserver.api.metadata.user.domain.User)
	 */
	public void putTenant(ExecutionContext context, Tenant aTenant) {
		RepoTenant existingTenant = getRepoTenant(aTenant.getId(), false);
		if (existingTenant == null) {
			existingTenant = (RepoTenant) getPersistentClassFactory().newObject(Tenant.class);
		}
		existingTenant.copyFromClient(aTenant, this);
		getHibernateTemplate().saveOrUpdate(existingTenant);
	}
	
	protected List getRepoSubTenants(ExecutionContext context, String parentTenantId) {
		RepoTenant parent = getRepoTenant(parentTenantId, false);
		if (parent == null || parent.getSubTenants() == null) {
			return new ArrayList(0);
		}
		return new ArrayList(parent.getSubTenants());
	}
	
	public List getSubTenantList(ExecutionContext context, Tenant parentTenant) {
		List persistentTenants = getRepoSubTenants(context, parentTenant.getId());
		return toClientTenantList(persistentTenants);
	}
	
	public List getSubTenantList(ExecutionContext context, String parnentTenantId) {
		List persistentTenants = getRepoSubTenants(context, parnentTenantId);
		return toClientTenantList(persistentTenants);
	}
	
	protected List<Tenant> toClientTenantList(List persistentTenants) {
		if (persistentTenants == null) {
			return null;
		}
		
		List<Tenant> tenants = new ArrayList<Tenant>(persistentTenants.size());
        for (Object persistentTenant1 : persistentTenants) {
            RepoTenant persistentTenant = (RepoTenant) persistentTenant1;
            Tenant tenant = toClientTenant(persistentTenant);
            tenants.add(tenant);
        }
		return tenants;
	}
	
	public Tenant getTenant(ExecutionContext context, String tenantId) {
		RepoTenant rTenant = getRepoTenant(tenantId, false);
		if (rTenant == null) {
			return null;
		}
        return toClientTenant(rTenant);
	}
	
	public void deleteTenant(ExecutionContext context, String tenantId) {
		RepoTenant tenant = getRepoTenant(tenantId, false);
		if (tenant != null) {
		   getHibernateTemplate().delete(tenant);
           getHibernateTemplate().flush();
		} else {
			if (log.isInfoEnabled()) {
				log.info("Tenant " + tenantId + " not found for deletion");
			}
		}
	}
	
	public void updateTenant(ExecutionContext context, Tenant aTenant) {
		putTenant(context, aTenant);
	}
	
//	public List getAllSubTenantList(ExecutionContext context, String parentTenantId) {
//		ArrayList allSubTenants = new ArrayList();
//		getAllSubTenantListPrivate(parentTenantId, allSubTenants);
//		return allSubTenants;
//	}

    public List getAllSubTenantList(ExecutionContext context, final String parentTenantId) {
        List subTenantList = new ArrayList();

        /* Retrieving parent tenant. */
        RepoTenant parent = getRepoTenant(parentTenantId, false);

        if (parent != null) {
            DetachedCriteria criteria = createSubTenantsCriteria(parent);
            subTenantList.addAll(getHibernateTemplate().findByCriteria(criteria));
        }

        return toClientTenantList(subTenantList);
    }

    @SuppressWarnings({"unchecked"})
    public List<String> getAllSubTenantIdList(ExecutionContext context, final String parentTenantId) {
        List<String> subTenantIdList = new ArrayList<String>();

        /* Retrieving parent tenant. */
        RepoTenant parent = getRepoTenant(parentTenantId, false);

        if (parent != null) {
            DetachedCriteria criteria = createSubTenantsCriteria(parent);
            criteria.setProjection(Projections.property("tenantId"));
            subTenantIdList.addAll(getHibernateTemplate().findByCriteria(criteria));
        }

        return subTenantIdList;
    }

    private DetachedCriteria createSubTenantsCriteria(RepoTenant parentTenant) {
        DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());
        String value = "/%";

        if ("/".equals(parentTenant.getTenantUri())) {
            /* Excluding root parent from sub list. */
            criteria.add(Restrictions.ne("tenantId", parentTenant.getTenantId()));
        } else {
            value = parentTenant.getTenantUri() + value;
        }

        criteria.add(Restrictions.like("tenantUri", value));
        return criteria;
    }

    private void getAllSubTenantListPrivate(String parentTenantId, ArrayList allSubTenants) {
		List subTenant = getSubTenantList(null, parentTenantId);
		for (int i=0; i<subTenant.size(); i++) {
			allSubTenants.add(subTenant.get(i));
			getAllSubTenantListPrivate(((Tenant)subTenant.get(i)).getId(), allSubTenants);
		}
	}
	
	public int getNumberOfTenants(ExecutionContext context) {
		List allTenants = getAllSubTenantList(null, ORGANIZATIONS);
		return allTenants.size();
	}
	
	public Tenant getDefaultTenant(ExecutionContext context) {
		List allTenants = getAllSubTenantList(null, ORGANIZATIONS);	
		if (allTenants.size() == 1) {
			return (Tenant)allTenants.get(0);
		} else {
			return null;
		}
	}
	
	
	public Tenant getTenantBasedOnTenantUri(ExecutionContext context, String tenantUri) {
		RepoTenant rTenant = getRepoTenantBasedOnTenantUri(context, tenantUri);
		if (rTenant == null) {
			return null;
		}
		Tenant tenant = toClientTenant(rTenant);
		return tenant;		
	}

	protected Tenant toClientTenant(RepoTenant rTenant) {
		Tenant tenant = (Tenant) objectMappingFactory.newObject(Tenant.class);
		rTenant.copyToClient(tenant);
		return tenant;
	}

	public String getUserOrgIdDelimiter() {
		return userOrgIdDelimiter;
	}

	public void setUserOrgIdDelimiter(String userOrgIdDelimiter) {
		this.userOrgIdDelimiter = userOrgIdDelimiter;
	}

	public Tenant getTenantBasedOnRepositoryUri(ExecutionContext context,
			String uri) {
		RepoTenant repoTenant = null;
		while (uri != null && repoTenant == null) {
			repoTenant = getRepoTenantBasedOnTenantFolderUri(uri);
			uri = RepositoryUtils.getParentPath(uri);
		}
		if (repoTenant == null || TenantService.ORGANIZATIONS.equals(repoTenant.getTenantId())) {
			return null;
		}
		Tenant tenant = toClientTenant(repoTenant);
		return tenant;		
	}
	
	protected RepoTenant getRepoTenantBasedOnTenantFolderUri(String folderUri) {
		DetachedCriteria criteria = DetachedCriteria.forClass(
				persistentTenantClass());
		criteria.add(Restrictions.eq("tenantFolderUri", folderUri));
		List tenantList = getHibernateTemplate().findByCriteria(criteria);
		RepoTenant tenant = null;		
		if (!tenantList.isEmpty()) {
			tenant = (RepoTenant) tenantList.get(0);
		}
		return tenant;
	}

	public boolean isMultiTenantEnvironment(ExecutionContext context) {
        DetachedCriteria criteria = DetachedCriteria.forClass(
                persistentTenantClass());
        criteria.add(Restrictions.ne("tenantUri", "/"));
        List tenantList = getHibernateTemplate().findByCriteria(criteria);
        return tenantList.size() > 1;
	}

	public RepoTenant getPersistentTenant(String tenantId, boolean required) {
		if (tenantId == null || tenantId.length() == 0) {
			return null;
		}
		
		return getRepoTenant(tenantId, required);
	}

    public RepoTenant getPersistentTenantByAlias(String tenantAlias, boolean required) {
        if (tenantAlias == null || tenantAlias.length() == 0) {
            return null;
        }

        return getRepoTenantByAlias(tenantAlias, required);
    }

    public String getUniqueTenantId(String proposedTenantId) {
        List similarTenantIdList;
        DetachedCriteria criteria = DetachedCriteria.forClass(
                persistentTenantClass());
        criteria.add(Restrictions.like("tenantId", proposedTenantId + "%"));
        List tenantList = getHibernateTemplate().findByCriteria(criteria);
        if (tenantList.isEmpty()) {
            similarTenantIdList = Collections.emptyList();
        } else {
            similarTenantIdList = new ArrayList(tenantList.size());
            for (int i = 0; i < tenantList.size(); i++) {
                similarTenantIdList.add(((RepoTenant) tenantList.get(i)).getTenantId());
            }
        }

        return getUniqueItem(proposedTenantId, similarTenantIdList);
    }

    private String getUniqueItem(String proposedItem, List similarItemsList) {
        if (similarItemsList.size() == 0) {
            return proposedItem;
        }

        for (int i = 0; i < similarItemsList.size(); i++) {
            String orgId = (String) similarItemsList.get(i);
            if (orgId.equalsIgnoreCase(proposedItem)) {
                break;
            }
            if (i == (similarItemsList.size() - 1)) {
                return proposedItem;
            }
        }

        int index = 1;
        boolean ready = false;
        while (!ready) {
            for (int i = 0; i < similarItemsList.size(); i++) {
                String orgId = (String) similarItemsList.get(i);
                if ((proposedItem + index).equalsIgnoreCase(orgId)) {
                    index++;
                    break;
                }
                if (i == (similarItemsList.size() - 1)) {
                    ready = true;
                    proposedItem = proposedItem + index;
                }
            }
        }

        return proposedItem;
    }

    public String getUniqueTenantAlias(String proposedAlias) {
        List similarAliasesAndTenantIdList;
        DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());
        criteria.add(Restrictions.like("tenantAlias", proposedAlias + "%"));
        List tenantAliasesList = getHibernateTemplate().findByCriteria(criteria);
        if (tenantAliasesList.isEmpty()) {
            similarAliasesAndTenantIdList = Collections.emptyList();
        } else {
            similarAliasesAndTenantIdList = new ArrayList(tenantAliasesList.size());
            for (int i = 0; i < tenantAliasesList.size(); i++) {
                similarAliasesAndTenantIdList.add(((RepoTenant) tenantAliasesList.get(i)).getTenantAlias());
            }
        }

        criteria = DetachedCriteria.forClass(persistentTenantClass());
        criteria.add(Restrictions.like("tenantId", proposedAlias + "%"));
        List tenantIdsList = getHibernateTemplate().findByCriteria(criteria);
        if (!tenantIdsList.isEmpty()) {
            if (similarAliasesAndTenantIdList.isEmpty()) {
                similarAliasesAndTenantIdList = new ArrayList(tenantIdsList.size());
            }
            for (int i = 0; i < tenantIdsList.size(); i++) {
                similarAliasesAndTenantIdList.add(((RepoTenant) tenantIdsList.get(i)).getTenantId());
            }
        }

        // Alias should be unique among all tenant IDs and tenant aliases.
        return getUniqueItem(proposedAlias, similarAliasesAndTenantIdList);
    }

    public int getNumberOfUsers(ExecutionContext context, String tenantId) {
        return createNumberOfUsersOrRolesCriteria(tenantId,
                getPersistentClassFactory().getImplementationClass(User.class));
    }

    public int getNumberOfRoles(ExecutionContext context, String tenantId) {
        return createNumberOfUsersOrRolesCriteria(tenantId,
                getPersistentClassFactory().getImplementationClass(Role.class));
    }

    private int createNumberOfUsersOrRolesCriteria(String tenantId, Class aClass) {
        Integer rowCount = 0;

        RepoTenant tenant = getRepoTenant(tenantId, false);
        if (tenant != null) {
            DetachedCriteria criteria = DetachedCriteria.forClass(aClass);
            criteria.createAlias("tenant", "t");

            criteria.add(Restrictions.or(
                    Restrictions.eq("t.tenantUri", tenant.getTenantUri()),
                    Restrictions.like("t.tenantUri",
                            tenant.getTenantUri().equals("/") ? "/%" : tenant.getTenantUri() + "/%")
            ));

            criteria.setProjection(Projections.count("id"));

            List results = getHibernateTemplate().findByCriteria(criteria);
            if (results != null && !results.isEmpty()) {

                rowCount = (Integer) results.get(0);
            }
        }

        return rowCount;
    }

    public int getSubTenantsCount(ExecutionContext context, String parentTenantId, String text) {
        DetachedCriteria criteria = createSearchTenantsCriteria(parentTenantId, text);

        criteria.setProjection(Projections.rowCount());

        List results = getHibernateTemplate().findByCriteria(criteria);

        if (results != null && !results.isEmpty()) {
            return (Integer) results.get(0);
        }

        return 0;
    }

    public List<Tenant> getSubTenants(ExecutionContext context, String parentTenantId, String text, int firstResult,
            int maxResults) {
        DetachedCriteria criteria = createSearchTenantsCriteria(parentTenantId, text);

        List results = getHibernateTemplate().findByCriteria(criteria, firstResult, maxResults);

        return toClientTenantList(results);
    }

    private DetachedCriteria createSearchTenantsCriteria(String parentTenantId, String text) {
        DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());

        criteria.createAlias("parent", "p");
        criteria.add(Restrictions.naturalId().set("p.tenantId", parentTenantId));

        if (text != null) {
            text = databaseCharactersEscapeResolver.getEscapedText(text.trim());
            if(text.length() > 0){
                Disjunction disjunction = Restrictions.disjunction();

//                disjunction.add(Restrictions.ilike("tenantId", "%" + text + "%"));
//                disjunction.add(Restrictions.ilike("tenantAlias", "%" + text + "%"));
//                disjunction.add(Restrictions.ilike("tenantName", "%" + text + "%"));
//                disjunction.add(Restrictions.ilike("tenantDesc", "%" + text + "%"));

                disjunction.add(new IlikeEscapeAwareExpression("tenantId", text, MatchMode.ANYWHERE));
                disjunction.add(new IlikeEscapeAwareExpression("tenantAlias", text, MatchMode.ANYWHERE));
                disjunction.add(new IlikeEscapeAwareExpression("tenantName", text, MatchMode.ANYWHERE));
                disjunction.add(new IlikeEscapeAwareExpression("tenantDesc", text, MatchMode.ANYWHERE));

                criteria.add(disjunction);
            }
        }
        
        return criteria;
    }

    public Map<String, Integer> getSubTenantsCountMap(List<String> tenantIds) {
        if (tenantIds == null || tenantIds.size() == 0) {
            return Collections.emptyMap();
        }
        
        DetachedCriteria criteria = DetachedCriteria.forClass(persistentTenantClass());

        criteria.createAlias("parent", "p");
        criteria.add(Restrictions.in("p.tenantId", tenantIds));

        criteria.setProjection(Projections.projectionList()
        .add(Projections.rowCount())
        .add(Projections.groupProperty("p.tenantId")));

        List results = getHibernateTemplate().findByCriteria(criteria);

        Map<String, Integer> subTenantCounts = new HashMap<String, Integer>(tenantIds.size(), 1);
        if (results != null && results.size() > 0) {
            for (Object result: results) {
                String tenantId = (String)((Object[])result)[1];
                Integer count = (Integer)((Object[])result)[0];

                subTenantCounts.put(tenantId, count);
            }
        }

        for (String tenantId: tenantIds) {
            if (!subTenantCounts.containsKey(tenantId)) {
                subTenantCounts.put(tenantId, 0);
            }
        }

        return subTenantCounts;
    }

    public void setTenantActiveTheme(ExecutionContext context, String tenantId, String themeName) {
        if (themeName == null) {
            throw new JSException("Theme name should not be null!");//TODO i18n
        }
        Tenant tenant = getTenant(context, tenantId);
        if (tenant == null) {
            throw new JSException("Tenant not found : \"" + tenantId + "\"");//TODO i18n
        }
        tenant.setTheme(themeName);
        putTenant(context, tenant);
    }
}
