/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.syncope.core.provisioning.java.utils;

import java.util.Map;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.lib.EntityTOUtils;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.AnyObjectTO;
import org.apache.syncope.common.lib.to.AnyTO;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.to.GroupTO;
import org.apache.syncope.common.lib.to.GroupableRelatableTO;
import org.apache.syncope.common.lib.to.MembershipTO;
import org.apache.syncope.common.lib.to.RelationshipTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.entity.AnyTemplate;
import org.apache.syncope.core.persistence.api.entity.group.Group;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class TemplateUtils {

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private GroupDAO groupDAO;

    private AttrTO evaluateAttr(final AttrTO template, final MapContext jexlContext) {
        AttrTO result = new AttrTO();
        result.setSchema(template.getSchema());

        if (template.getValues() != null && !template.getValues().isEmpty()) {
            for (String value : template.getValues()) {
                String evaluated = JexlUtils.evaluate(value, jexlContext);
                if (StringUtils.isNotBlank(evaluated)) {
                    result.getValues().add(evaluated);
                }
            }
        }

        return result;
    }

    private void fill(final AnyTO anyTO, final AnyTO template) {
        MapContext jexlContext = new MapContext();
        JexlUtils.addFieldsToContext(anyTO, jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getPlainAttrs(), jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getDerAttrs(), jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getVirAttrs(), jexlContext);

        if (template.getRealm() != null) {
            String evaluated = JexlUtils.evaluate(template.getRealm(), jexlContext);
            if (StringUtils.isNotBlank(evaluated)) {
                anyTO.setRealm(evaluated);
            }
        }

        Map<String, AttrTO> currentAttrMap = EntityTOUtils.buildAttrMap(anyTO.getPlainAttrs());
        for (AttrTO templatePlainAttr : template.getPlainAttrs()) {
            if (!templatePlainAttr.getValues().isEmpty()
                    && (!currentAttrMap.containsKey(templatePlainAttr.getSchema())
                    || currentAttrMap.get(templatePlainAttr.getSchema()).getValues().isEmpty())) {

                AttrTO evaluated = evaluateAttr(templatePlainAttr, jexlContext);
                if (!evaluated.getValues().isEmpty()) {
                    anyTO.getPlainAttrs().add(evaluated);
                    jexlContext.set(evaluated.getSchema(), evaluated.getValues().get(0));
                }
            }
        }

        currentAttrMap = EntityTOUtils.buildAttrMap(anyTO.getDerAttrs());
        for (AttrTO templateDerAttr : template.getDerAttrs()) {
            if (!currentAttrMap.containsKey(templateDerAttr.getSchema())) {
                anyTO.getDerAttrs().add(templateDerAttr);
            }
        }

        currentAttrMap = EntityTOUtils.buildAttrMap(anyTO.getVirAttrs());
        for (AttrTO templateVirAttr : template.getVirAttrs()) {
            if (!templateVirAttr.getValues().isEmpty()
                    && (!currentAttrMap.containsKey(templateVirAttr.getSchema())
                    || currentAttrMap.get(templateVirAttr.getSchema()).getValues().isEmpty())) {

                AttrTO evaluated = evaluateAttr(templateVirAttr, jexlContext);
                if (!evaluated.getValues().isEmpty()) {
                    anyTO.getVirAttrs().add(evaluated);
                    jexlContext.set(evaluated.getSchema(), evaluated.getValues().get(0));
                }
            }
        }

        anyTO.getResources().addAll(template.getResources());

        anyTO.getAuxClasses().addAll(template.getAuxClasses());
    }

    private void fillRelationships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
        for (RelationshipTO relationship : template.getRelationships()) {
            if (any.getRelationship(relationship.getType(), relationship.getOtherEndKey()) == null) {
                any.getRelationships().add(relationship);
            }
        }
    }

    private void fillMemberships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
        for (MembershipTO membership : template.getMemberships()) {
            if (any.getMembership(membership.getGroupKey()) == null) {
                any.getMemberships().add(membership);
            }
        }
    }

    @Transactional(readOnly = true)
    public <T extends AnyTO> void apply(final T anyTO, final AnyTemplate anyTemplate) {
        if (anyTemplate != null) {
            apply(anyTO, anyTemplate.get());
        }
    }

    @Transactional(readOnly = true)
    public <T extends AnyTO> void apply(final T anyTO, final AnyTO template) {
        fill(anyTO, template);

        MapContext jexlContext = new MapContext();
        JexlUtils.addFieldsToContext(anyTO, jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getPlainAttrs(), jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getDerAttrs(), jexlContext);
        JexlUtils.addAttrTOsToContext(anyTO.getVirAttrs(), jexlContext);

        if (template instanceof AnyObjectTO) {
            fillRelationships((GroupableRelatableTO) anyTO, ((GroupableRelatableTO) template));
            fillMemberships((GroupableRelatableTO) anyTO, ((GroupableRelatableTO) template));
        } else if (template instanceof UserTO) {
            if (StringUtils.isNotBlank(((UserTO) template).getUsername())) {
                String evaluated = JexlUtils.evaluate(((UserTO) template).getUsername(), jexlContext);
                if (StringUtils.isNotBlank(evaluated)) {
                    ((UserTO) anyTO).setUsername(evaluated);
                }
            }

            if (StringUtils.isNotBlank(((UserTO) template).getPassword())) {
                String evaluated = JexlUtils.evaluate(((UserTO) template).getPassword(), jexlContext);
                if (StringUtils.isNotBlank(evaluated)) {
                    ((UserTO) anyTO).setPassword(evaluated);
                }
            }

            fillRelationships((GroupableRelatableTO) anyTO, ((GroupableRelatableTO) template));
            fillMemberships((GroupableRelatableTO) anyTO, ((GroupableRelatableTO) template));
            ((UserTO) anyTO).getRoles().addAll(((UserTO) template).getRoles());
        } else if (template instanceof GroupTO) {
            if (StringUtils.isNotBlank(((GroupTO) template).getName())) {
                String evaluated = JexlUtils.evaluate(((GroupTO) template).getName(), jexlContext);
                if (StringUtils.isNotBlank(evaluated)) {
                    ((GroupTO) anyTO).setName(evaluated);
                }
            }

            if (((GroupTO) template).getUserOwner() != null) {
                final User userOwner = userDAO.find(((GroupTO) template).getUserOwner());
                if (userOwner != null) {
                    ((GroupTO) anyTO).setUserOwner(userOwner.getKey());
                }
            }
            if (((GroupTO) template).getGroupOwner() != null) {
                final Group groupOwner = groupDAO.find(((GroupTO) template).getGroupOwner());
                if (groupOwner != null) {
                    ((GroupTO) anyTO).setGroupOwner(groupOwner.getKey());
                }
            }
        }
    }

    public void check(final Map<String, AnyTO> templates, final ClientExceptionType clientExceptionType) {
        SyncopeClientException sce = SyncopeClientException.build(clientExceptionType);

        for (Map.Entry<String, AnyTO> entry : templates.entrySet()) {
            for (AttrTO attrTO : entry.getValue().getPlainAttrs()) {
                if (!attrTO.getValues().isEmpty() && !JexlUtils.isExpressionValid(attrTO.getValues().get(0))) {
                    sce.getElements().add("Invalid JEXL: " + attrTO.getValues().get(0));
                }
            }

            for (AttrTO attrTO : entry.getValue().getVirAttrs()) {
                if (!attrTO.getValues().isEmpty() && !JexlUtils.isExpressionValid(attrTO.getValues().get(0))) {
                    sce.getElements().add("Invalid JEXL: " + attrTO.getValues().get(0));
                }
            }

            if (entry.getValue() instanceof UserTO) {
                UserTO template = (UserTO) entry.getValue();
                if (StringUtils.isNotBlank(template.getUsername())
                        && !JexlUtils.isExpressionValid(template.getUsername())) {

                    sce.getElements().add("Invalid JEXL: " + template.getUsername());
                }
                if (StringUtils.isNotBlank(template.getPassword())
                        && !JexlUtils.isExpressionValid(template.getPassword())) {

                    sce.getElements().add("Invalid JEXL: " + template.getPassword());
                }
            } else if (entry.getValue() instanceof GroupTO) {
                GroupTO template = (GroupTO) entry.getValue();
                if (StringUtils.isNotBlank(template.getName())
                        && !JexlUtils.isExpressionValid(template.getName())) {

                    sce.getElements().add("Invalid JEXL: " + template.getName());
                }
            }
        }

        if (!sce.isEmpty()) {
            throw sce;
        }
    }
}
