/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.usage;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.ibatis.IdsEntity;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.domain.query.CannedQueryDAO;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.service.cmr.admin.RepoUsage;
import org.alfresco.service.cmr.admin.RepoUsage.LicenseMode;
import org.alfresco.service.cmr.admin.RepoUsage.UsageType;
import org.alfresco.service.cmr.admin.RepoUsageStatus;
import org.alfresco.service.cmr.admin.RepoUsageStatus.RepoUsageLevel;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.DateUtil;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;

/**
 * Low-level implementation to answer repository usage queries
 * 
 * @author Derek Hulley
 * @since 3.5
 */
public class RepoUsageComponentImpl implements RepoUsageComponent
{
    private static final String QUERY_NS = "alfresco.query.usages";
    private static final String QUERY_SELECT_COUNT_PERSONS_NOT_DISABLED = "select_CountPersonsNotDisabled";
    private static final String QUERY_SELECT_COUNT_DOCUMENTS = "select_CountDocuments";
    
    private static Log logger = LogFactory.getLog(RepoUsageComponentImpl.class);
    
    private TransactionService transactionService;
    private AuthorityService authorityService;
    private AttributeService attributeService;
    private DictionaryService dictionaryService;
    private CannedQueryDAO cannedQueryDAO;
    private QNameDAO qnameDAO;
    
    private RepoUsage restrictions;
    private ReadLock restrictionsReadLock;
    private WriteLock restrictionsWriteLock;
    private Set<RestrictionObserver> restrictionObservers = new HashSet<RestrictionObserver>();
    
    /**
     * Defaults
     */
    public RepoUsageComponentImpl()
    {
        this.restrictions = new RepoUsage(null, null, null, LicenseMode.UNKNOWN, null, false);
        
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        restrictionsReadLock = lock.readLock();
        restrictionsWriteLock = lock.writeLock();
    }

    /**
     * @param transactionService        service that tells if the server is read-only or not
     */
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }

    /**
     * @param authorityService          service to check for admin rights
     */
    public void setAuthorityService(AuthorityService authorityService)
    {
        this.authorityService = authorityService;
    }

    /**
     * @param attributeService          service used to store usage attributes
     */
    public void setAttributeService(AttributeService attributeService)
    {
        this.attributeService = attributeService;
    }

    /**
     * @param dictionaryService         component to resolve types and subtypes
     */
    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }

    /**
     * @param cannedQueryDAO            DAO for executing queries
     */
    public void setCannedQueryDAO(CannedQueryDAO cannedQueryDAO)
    {
        this.cannedQueryDAO = cannedQueryDAO;
    }

    /**
     * @param qnameDAO                  DAO for getting IDs of QNames
     */
    public void setQnameDAO(QNameDAO qnameDAO)
    {
        this.qnameDAO = qnameDAO;
    }

    @Override
    public void observeRestrictions(RestrictionObserver observer)
    {
        restrictionObservers.add(observer);
    }
    
    /**
     * Check that all properties are properly set
     */
    public void init()
    {
        PropertyCheck.mandatory(this, "transactionService", transactionService);
        PropertyCheck.mandatory(this, "authorityService", authorityService);
        PropertyCheck.mandatory(this, "attributeService", attributeService);
        PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
        PropertyCheck.mandatory(this, "cannedQueryDAO", cannedQueryDAO);
        PropertyCheck.mandatory(this, "qnameDAO", qnameDAO);
    }
    
    /**
     * Checks that the 'System' user is active in a read-write txn.
     */
    private final void checkTxnState(TxnReadState txnStateNeeded)
    {
        switch (txnStateNeeded)
        {
            case TXN_READ_WRITE:
                if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE)
                {
                    throw AlfrescoRuntimeException.create("system.usage.err.no_txn_readwrite");
                }
                break;
            case TXN_READ_ONLY:
                if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_NONE)
                {
                    throw AlfrescoRuntimeException.create("system.usage.err.no_txn");
                }
                break;
        }
    }

    @Override
    public void setRestrictions(RepoUsage restrictions)
    {
        checkTxnState(TxnReadState.TXN_NONE);
        restrictionsWriteLock.lock();
        try
        {
            this.restrictions = restrictions;
        }
        finally
        {
            restrictionsWriteLock.unlock();
        }
        
        // Fire observers
        for(RestrictionObserver observer : restrictionObservers )
        {
            observer.onChangeRestriction(restrictions);
        }
    }

    @Override
    public RepoUsage getRestrictions()
    {
        // No need to check txn state and any user can get this info.
        restrictionsReadLock.lock();
        try
        {
            return restrictions;
        }
        finally
        {
            restrictionsReadLock.unlock();
        }
    }

    @Override
    public boolean updateUsage(UsageType usageType)
    {
        return updateUsage(usageType, false);
    }

    @Override
    public boolean resetUsage(UsageType usageType)
    {
        return updateUsage(usageType, true);
    }

    private boolean updateUsage(UsageType usageType, boolean reset)
    {
        checkTxnState(TxnReadState.TXN_READ_WRITE);
        
        boolean updateUsers = false;
        boolean updateDocuments = false;
        switch (usageType)
        {
            case USAGE_DOCUMENTS:
                updateDocuments = true;
                break;
            case USAGE_USERS:
                updateUsers = true;
                break;
            case USAGE_ALL:
                updateUsers = true;
                updateDocuments = true;
        }
        
        if (updateUsers && !updateUsers(reset))
        {
            return false;
        }
        if (updateDocuments && !updateDocuments(reset))
        {
            return false;
        }
        
        // Done
        if (logger.isDebugEnabled())
        {
            RepoUsage usage = getUsageImpl();
            logger.debug("Updated repo usage: " + usage);
        }
        // The update succeeded and the locks held
        return true;
    }
    
    /**
     * Update number of users with appropriate locking
     */
    private boolean updateUsers(boolean reset)
    {
        Long userCount = 0L;

        if (!reset)
        {
            // Count users
            IdsEntity idsParam = new IdsEntity();
            idsParam.setIdOne(qnameDAO.getOrCreateQName(ContentModel.ASPECT_PERSON_DISABLED).getFirst());
            idsParam.setIdTwo(qnameDAO.getOrCreateQName(ContentModel.TYPE_PERSON).getFirst());
            userCount = cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_COUNT_PERSONS_NOT_DISABLED, idsParam);

            // We subtract one to cater for 'guest', which is implicit
            userCount = userCount > 0L ? userCount - 1L : 0L;

        }
        attributeService.setAttribute(
                new Long(System.currentTimeMillis()),
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_LAST_UPDATE_USERS);
        attributeService.setAttribute(
                userCount,
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_USERS);
        // Success
        return true;
    }
    
    /**
     * Update number of documents with appropriate locking
     */
    private boolean updateDocuments(boolean reset)
    {
        Long documentCount = 0L;

        if (!reset)
        {
            // Count documents
            Set<QName> searchTypeQNames = new HashSet<QName>(11);
            Collection<QName> qnames = dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true);
            searchTypeQNames.addAll(qnames);
            searchTypeQNames.add(ContentModel.TYPE_CONTENT);
            qnames = dictionaryService.getSubTypes(ContentModel.TYPE_LINK, true);
            searchTypeQNames.addAll(qnames);
            searchTypeQNames.add(ContentModel.TYPE_LINK);
            Set<Long> searchTypeQNameIds = qnameDAO.convertQNamesToIds(searchTypeQNames, false);
            IdsEntity idsParam = new IdsEntity();
            idsParam.setIds(new ArrayList<Long>(searchTypeQNameIds));
            documentCount = cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_COUNT_DOCUMENTS, idsParam);
        }
        attributeService.setAttribute(
                new Long(System.currentTimeMillis()),
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_LAST_UPDATE_DOCUMENTS);
        attributeService.setAttribute(
                documentCount,
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_DOCUMENTS);
        // Success
        return true;
    }

    /**
     * Build the usage component.  Protect with a read lock, transaction check and authentication check.
     */
    private RepoUsage getUsageImpl()
    {
        // Fetch persisted usage data
        Long lastUpdateUsers = (Long) attributeService.getAttribute(
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_LAST_UPDATE_USERS);
        Long users = (Long) attributeService.getAttribute(
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_USERS);
        Long lastUpdateDocuments = (Long) attributeService.getAttribute(
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_LAST_UPDATE_DOCUMENTS);
        Long documents = (Long) attributeService.getAttribute(
                KEY_USAGE_ROOT, KEY_USAGE_CURRENT, KEY_USAGE_DOCUMENTS);

        final Long lastUpdate;
        if (lastUpdateUsers == null)
        {
            lastUpdate = lastUpdateDocuments;
        }
        else if (lastUpdateDocuments == null)
        {
            lastUpdate = lastUpdateUsers;
        }
        else if (lastUpdateDocuments.compareTo(lastUpdateUsers) > 0)
        {
            lastUpdate = lastUpdateDocuments;
        }
        else
        {
            lastUpdate = lastUpdateUsers;
        }
        
        // Combine with current restrictions
        RepoUsage usage = new RepoUsage(
                lastUpdate,
                users,
                documents,
                restrictions.getLicenseMode(),
                restrictions.getLicenseExpiryDate(),
                transactionService.getAllowWrite() == false);
        return usage;
    }

    @Override
    public RepoUsage getUsage()
    {
        checkTxnState(TxnReadState.TXN_READ_ONLY);
        restrictionsReadLock.lock();
        try
        {
            // Combine with current restrictions
            RepoUsage usage = getUsageImpl();
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug("Retrieved repo usage: " + usage);
            }
            return usage;
        }
        finally
        {
            restrictionsReadLock.unlock();
        }
    }
    
    /**
     * Calculate and retrieve full status alerts based on the usage and license expiry state.
     * 
     * @return              Returns the usage status bean
     */
    public RepoUsageStatus getUsageStatus()
    {
        RepoUsage usage = getUsage();
        RepoUsage restrictions = getRestrictions();
        
        RepoUsageLevel level = RepoUsageLevel.OK;
        List<String> warnings = new ArrayList<String>(1);
        List<String> errors = new ArrayList<String>(1);
        
        // Check users
        long usersCurrent = usage.getUsers() == null ? 0L : usage.getUsers();
        long usersMax = restrictions.getUsers() == null ? Long.MAX_VALUE : restrictions.getUsers();
        if (usersCurrent > usersMax)
        {
            errors.add(I18NUtil.getMessage("system.usage.err.limit_users_exceeded", usersMax, usersCurrent));
            // MNT-12712 changed this from LOCKED DOWN
            level = RepoUsageLevel.WARN_ALL;
        }
        else if (usersCurrent == usersMax)
        {
            warnings.add(I18NUtil.getMessage("system.usage.warn.limit_users_reached", usersMax, usersCurrent));
            level = RepoUsageLevel.WARN_ALL;
        }
        else if (usersCurrent >= (0.9 * usersMax) || usersCurrent >= (usersMax - 1))
        {
            warnings.add(I18NUtil.getMessage("system.usage.warn.limit_users_approached", usersMax, usersCurrent));
            level = RepoUsageLevel.WARN_ADMIN;
        }
        
        // Check documents
        long documentsCurrent = usage.getDocuments() == null ? 0L : usage.getDocuments();
        long documentsMax = restrictions.getDocuments() == null ? Long.MAX_VALUE : restrictions.getDocuments();
        if (documentsCurrent > documentsMax)
        {
            errors.add(I18NUtil.getMessage("system.usage.err.limit_documents_exceeded", documentsMax, documentsCurrent));
            level = RepoUsageLevel.LOCKED_DOWN;
        }
        else if (documentsCurrent > 0.99 * documentsMax)
        {
            warnings.add(I18NUtil.getMessage("system.usage.warn.limit_documents_reached", documentsMax, documentsCurrent));
            if (level.ordinal() < RepoUsageLevel.WARN_ALL.ordinal())
            {
                level = RepoUsageLevel.WARN_ALL;
            }
        }
        else if (documentsCurrent > 0.9 * documentsMax)
        {
            warnings.add(I18NUtil.getMessage("system.usage.warn.limit_documents_approached", documentsMax, documentsCurrent));
            if (level.ordinal() < RepoUsageLevel.WARN_ADMIN.ordinal())
            {
                level = RepoUsageLevel.WARN_ADMIN;
            }
        }
        
        // Check the license expiry
        Long licenseExpiryDate = restrictions.getLicenseExpiryDate();
        if (licenseExpiryDate != null)
        {
            int remainingDays = DateUtil.calculateDays(System.currentTimeMillis(), licenseExpiryDate);
            if (remainingDays <= 0)
            {
                errors.add(I18NUtil.getMessage("system.usage.err.limit_license_expired"));
                level = RepoUsageLevel.LOCKED_DOWN;
            }
            else if (remainingDays <= 7)
            {
                warnings.add(I18NUtil.getMessage("system.usage.err.limit_license_expiring", remainingDays));
                if (level.ordinal() < RepoUsageLevel.WARN_ADMIN.ordinal())
                {
                    level = RepoUsageLevel.WARN_ALL;
                }
            }
            else if (remainingDays <= 21)
            {
                warnings.add(I18NUtil.getMessage("system.usage.err.limit_license_expiring", remainingDays));
                if (level.ordinal() < RepoUsageLevel.WARN_ALL.ordinal())
                {
                    level = RepoUsageLevel.WARN_ADMIN;
                }
            }
        }
        
        RepoUsageStatus status = new RepoUsageStatus(restrictions, usage, level, warnings, errors);
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Usage status generated: " + status);
        }
        return status;
    }
}
