/*
 * 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 java.util.Objects;
import java.util.Optional;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.lib.Attr;
import org.apache.syncope.common.lib.EntityTOUtils;
import org.apache.syncope.common.lib.RealmMember;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.request.GroupCR;
import org.apache.syncope.common.lib.request.UserCR;
import org.apache.syncope.common.lib.to.AnyObjectTO;
import org.apache.syncope.common.lib.to.AnyTO;
import org.apache.syncope.common.lib.to.GroupTO;
import org.apache.syncope.common.lib.to.GroupableRelatableTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
import org.springframework.transaction.annotation.Transactional;

public class TemplateUtils {

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

        templates.values().forEach(value -> {
            value.getPlainAttrs().stream().
                    filter(attrTO -> !attrTO.getValues().isEmpty()
                    && !JexlUtils.isExpressionValid(attrTO.getValues().get(0))).
                    forEachOrdered(attrTO -> sce.getElements().add("Invalid JEXL: " + attrTO.getValues().get(0)));

            value.getVirAttrs().stream().
                    filter(attrTO -> !attrTO.getValues().isEmpty()
                    && !JexlUtils.isExpressionValid(attrTO.getValues().get(0))).
                    forEachOrdered((attrTO) -> sce.getElements().add("Invalid JEXL: " + attrTO.getValues().get(0)));

            switch (value) {
                case UserTO template -> {
                    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());
                    }
                }
                case GroupTO template -> {
                    if (StringUtils.isNotBlank(template.getName())
                            && !JexlUtils.isExpressionValid(template.getName())) {

                        sce.getElements().add("Invalid JEXL: " + template.getName());
                    }
                }
                default -> {
                }
            }
        });

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

    protected static Attr evaluateAttr(final Attr template, final MapContext jexlContext) {
        Attr result = new Attr();
        result.setSchema(template.getSchema());

        if (template.getValues() != null && !template.getValues().isEmpty()) {
            template.getValues().forEach(value -> {
                String evaluated = JexlUtils.evaluateExpr(value, jexlContext).toString();
                if (StringUtils.isNotBlank(evaluated)) {
                    result.getValues().add(evaluated);
                }
            });
        }

        return result;
    }

    protected static void fill(final RealmMember realmMember, final RealmMember template) {
        MapContext jexlContext = new MapContext();
        JexlUtils.addFieldsToContext(realmMember, jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getPlainAttrs(), jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getDerAttrs(), jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getVirAttrs(), jexlContext);

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

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

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

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

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

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

        realmMember.getResources().addAll(template.getResources());

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

    protected static void fillRelationships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
        template.getRelationships().stream().
                filter(relationship -> any.getRelationship(
                relationship.getOtherEndKey(), relationship.getOtherEndKey()).isEmpty()).
                forEachOrdered(relationship -> any.getRelationships().add(relationship));
    }

    protected static void fillMemberships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
        template.getMemberships().stream().
                filter(membership -> any.getMembership(membership.getGroupKey()).isEmpty()).
                forEachOrdered(membership -> any.getMemberships().add(membership));
    }

    protected final UserDAO userDAO;

    protected final GroupDAO groupDAO;

    public TemplateUtils(final UserDAO userDAO, final GroupDAO groupDAO) {
        this.userDAO = userDAO;
        this.groupDAO = groupDAO;
    }

    @Transactional(readOnly = true)
    public void apply(final RealmMember realmMember, final AnyTO template) {
        fill(realmMember, template);

        MapContext jexlContext = new MapContext();
        JexlUtils.addFieldsToContext(realmMember, jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getPlainAttrs(), jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getDerAttrs(), jexlContext);
        JexlUtils.addAttrsToContext(realmMember.getVirAttrs(), jexlContext);

        switch (template) {
            case AnyObjectTO anyObjectTO -> {
                fillRelationships((GroupableRelatableTO) realmMember, anyObjectTO);
                fillMemberships((GroupableRelatableTO) realmMember, anyObjectTO);
            }

            case UserTO userTO -> {
                if (StringUtils.isNotBlank(userTO.getUsername())) {
                    String evaluated = JexlUtils.evaluateExpr(userTO.getUsername(), jexlContext).toString();
                    if (StringUtils.isNotBlank(evaluated)) {
                        switch (realmMember) {
                            case UserTO urm ->
                                urm.setUsername(evaluated);
                            case UserCR urm ->
                                urm.setUsername(evaluated);
                            default -> {
                            }
                        }
                    }
                }

                if (StringUtils.isNotBlank(userTO.getPassword())) {
                    String evaluated = JexlUtils.evaluateExpr(userTO.getPassword(), jexlContext).toString();
                    if (StringUtils.isNotBlank(evaluated)) {
                        switch (realmMember) {
                            case UserTO urm ->
                                urm.setPassword(evaluated);
                            case UserCR urm ->
                                urm.setPassword(evaluated);
                            default -> {
                            }
                        }
                    }
                }

                if (userTO.isMustChangePassword()) {
                    switch (realmMember) {
                        case UserTO urm ->
                            urm.setMustChangePassword(true);
                        case UserCR urm ->
                            urm.setMustChangePassword(true);
                        default -> {
                        }
                    }
                }

                fillRelationships((GroupableRelatableTO) realmMember, ((GroupableRelatableTO) template));
                fillMemberships((GroupableRelatableTO) realmMember, ((GroupableRelatableTO) template));

                userTO.getRoles().forEach(role -> {
                    if (realmMember instanceof UserTO urm && !urm.getRoles().contains(role)) {
                        urm.getRoles().add(role);
                    } else if (realmMember instanceof UserCR urm && !urm.getRoles().contains(role)) {
                        urm.getRoles().add(role);
                    }
                });

                userTO.getLinkedAccounts().forEach(account -> {
                    if (realmMember instanceof UserTO urm && urm.getLinkedAccounts().stream().
                            noneMatch(a -> Objects.equals(account.getConnObjectKeyValue(), a.getConnObjectKeyValue())
                            && Objects.equals(account.getResource(), a.getResource()))) {

                        urm.getLinkedAccounts().add(account);
                    } else if (realmMember instanceof UserCR urm && urm.getLinkedAccounts().stream().
                            noneMatch(a -> Objects.equals(account.getConnObjectKeyValue(), a.getConnObjectKeyValue())
                            && Objects.equals(account.getResource(), a.getResource()))) {

                        urm.getLinkedAccounts().add(account);
                    }
                });
            }

            case GroupTO groupTO -> {
                if (StringUtils.isNotBlank(groupTO.getName())) {
                    String evaluated = JexlUtils.evaluateExpr(groupTO.getName(), jexlContext).toString();
                    if (StringUtils.isNotBlank(evaluated)) {
                        switch (realmMember) {
                            case GroupTO grm ->
                                grm.setName(evaluated);
                            case GroupCR grm ->
                                grm.setName(evaluated);
                            default -> {
                            }
                        }
                    }
                }

                Optional.ofNullable(groupTO.getUserOwner()).flatMap(userDAO::findById).ifPresent(userOwner -> {
                    switch (realmMember) {
                        case GroupTO grm ->
                            grm.setUserOwner(userOwner.getKey());
                        case GroupCR grm ->
                            grm.setUserOwner(userOwner.getKey());
                        default -> {
                        }
                    }
                });
                Optional.ofNullable(groupTO.getGroupOwner()).flatMap(groupDAO::findById).ifPresent(groupOwner -> {
                    switch (realmMember) {
                        case GroupTO grm ->
                            grm.setGroupOwner(groupOwner.getKey());
                        case GroupCR grm ->
                            grm.setGroupOwner(groupOwner.getKey());
                        default -> {
                        }
                    }
                });

                Optional.ofNullable(groupTO.getUDynMembershipCond()).ifPresent(udynMembershipCond -> {
                    switch (realmMember) {
                        case GroupTO grm ->
                            grm.setUDynMembershipCond(udynMembershipCond);
                        case GroupCR grm ->
                            grm.setUDynMembershipCond(udynMembershipCond);
                        default -> {
                        }
                    }
                });

                groupTO.getADynMembershipConds().forEach((anyType, cond) -> {
                    if (realmMember instanceof GroupTO grm && grm.getADynMembershipConds().containsKey(anyType)) {
                        grm.getADynMembershipConds().put(anyType, cond);
                    } else if (realmMember instanceof GroupCR grm
                            && !grm.getADynMembershipConds().containsKey(anyType)) {

                        grm.getADynMembershipConds().put(anyType, cond);
                    }
                });

                groupTO.getTypeExtensions().forEach(typeExt -> {
                    if (realmMember instanceof GroupTO grm && !grm.getTypeExtensions().contains(typeExt)) {
                        grm.getTypeExtensions().add(typeExt);
                    } else if (realmMember instanceof GroupCR grm && !grm.getTypeExtensions().contains(typeExt)) {
                        grm.getTypeExtensions().add(typeExt);
                    }
                });
            }

            default -> {
            }
        }
    }
}
