/*
 * 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.logic;

import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.ProvisioningResult;
import org.apache.syncope.common.lib.to.RealmTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.syncope.common.lib.types.ResourceOperation;
import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
import org.apache.syncope.core.persistence.api.dao.CASSPClientAppDAO;
import org.apache.syncope.core.persistence.api.dao.DuplicateException;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.OIDCRPClientAppDAO;
import org.apache.syncope.core.persistence.api.dao.RealmDAO;
import org.apache.syncope.core.persistence.api.dao.SAML2SPClientAppDAO;
import org.apache.syncope.core.persistence.api.dao.TaskDAO;
import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.entity.Realm;
import org.apache.syncope.core.provisioning.api.PropagationByResource;
import org.apache.syncope.core.provisioning.api.data.RealmDataBinder;
import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.identityconnectors.framework.common.objects.Attribute;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {

    protected final RealmDAO realmDAO;

    protected final AnySearchDAO searchDAO;

    protected final TaskDAO taskDAO;

    protected final CASSPClientAppDAO casSPClientAppDAO;

    protected final OIDCRPClientAppDAO oidcRPClientAppDAO;

    protected final SAML2SPClientAppDAO saml2SPClientAppDAO;

    protected final RealmDataBinder binder;

    protected final PropagationManager propagationManager;

    protected final PropagationTaskExecutor taskExecutor;

    public RealmLogic(
            final RealmDAO realmDAO,
            final AnySearchDAO searchDAO,
            final TaskDAO taskDAO,
            final CASSPClientAppDAO casSPClientAppDAO,
            final OIDCRPClientAppDAO oidcRPClientAppDAO,
            final SAML2SPClientAppDAO saml2SPClientAppDAO,
            final RealmDataBinder binder,
            final PropagationManager propagationManager,
            final PropagationTaskExecutor taskExecutor) {

        this.realmDAO = realmDAO;
        this.searchDAO = searchDAO;
        this.taskDAO = taskDAO;
        this.casSPClientAppDAO = casSPClientAppDAO;
        this.oidcRPClientAppDAO = oidcRPClientAppDAO;
        this.saml2SPClientAppDAO = saml2SPClientAppDAO;
        this.binder = binder;
        this.propagationManager = propagationManager;
        this.taskExecutor = taskExecutor;
    }

    @PreAuthorize("hasRole('" + IdRepoEntitlement.REALM_LIST + "')")
    @Transactional(readOnly = true)
    public Pair<Integer, List<RealmTO>> search(final String keyword, final String base) {
        Realm baseRealm = base == null ? realmDAO.getRoot() : realmDAO.findByFullPath(base);
        if (baseRealm == null) {
            LOG.error("Could not find realm '" + base + "'");

            throw new NotFoundException(base);
        }

        Set<String> roots = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_LIST).stream().
                filter(auth -> auth.startsWith(baseRealm.getFullPath())).collect(Collectors.toSet());

        Set<Realm> match = realmDAO.findMatching(keyword).stream().
                filter(realm -> roots.stream().anyMatch(root -> realm.getFullPath().startsWith(root))).
                collect(Collectors.toSet());

        int descendants = Math.toIntExact(
                match.stream().flatMap(realm -> realmDAO.findDescendants(realm).stream()).distinct().count());

        return Pair.of(
                descendants,
                match.stream().map(realm -> binder.getRealmTO(realm, true)).
                        sorted(Comparator.comparing(RealmTO::getFullPath)).
                        collect(Collectors.toList()));
    }

    @PreAuthorize("isAuthenticated()")
    @Transactional(readOnly = true)
    public List<RealmTO> list(final String fullPath) {
        Realm realm = realmDAO.findByFullPath(fullPath);
        if (realm == null) {
            LOG.error("Could not find realm '" + fullPath + '\'');

            throw new NotFoundException(fullPath);
        }

        boolean admin = AuthContextUtils.getAuthorizations().keySet().contains(IdRepoEntitlement.REALM_LIST);
        return realmDAO.findDescendants(realm).stream().
                map(descendant -> binder.getRealmTO(descendant, admin)).collect(Collectors.toList());
    }

    @PreAuthorize("hasRole('" + IdRepoEntitlement.REALM_CREATE + "')")
    public ProvisioningResult<RealmTO> create(final String parentPath, final RealmTO realmTO) {
        Realm parent;
        if (StringUtils.isBlank(realmTO.getParent())) {
            parent = realmDAO.findByFullPath(parentPath);
            if (parent == null) {
                LOG.error("Could not find parent realm " + parentPath);

                throw new NotFoundException(parentPath);
            }

            realmTO.setParent(parent.getFullPath());
        } else {
            parent = realmDAO.find(realmTO.getParent());
            if (parent == null) {
                LOG.error("Could not find parent realm " + realmTO.getParent());

                throw new NotFoundException(realmTO.getParent());
            }

            if (!parent.getFullPath().equals(parentPath)) {
                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPath);
                sce.getElements().add("Mismatching parent realm: " + parentPath + " Vs " + parent.getFullPath());
                throw sce;
            }
        }

        String fullPath = StringUtils.appendIfMissing(parent.getFullPath(), "/") + realmTO.getName();
        if (realmDAO.findByFullPath(fullPath) != null) {
            throw new DuplicateException(fullPath);
        }

        Realm realm = realmDAO.save(binder.create(parent, realmTO));
        PropagationByResource<String> propByRes = new PropagationByResource<>();
        propByRes.addAll(ResourceOperation.CREATE, realm.getResourceKeys());
        List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
        PropagationReporter propagationReporter =
                taskExecutor.execute(taskInfos, false, AuthContextUtils.getUsername());

        ProvisioningResult<RealmTO> result = new ProvisioningResult<>();
        result.setEntity(binder.getRealmTO(realm, true));
        result.getPropagationStatuses().addAll(propagationReporter.getStatuses());

        return result;
    }

    @PreAuthorize("hasRole('" + IdRepoEntitlement.REALM_UPDATE + "')")
    public ProvisioningResult<RealmTO> update(final RealmTO realmTO) {
        Realm realm = realmDAO.findByFullPath(realmTO.getFullPath());
        if (realm == null) {
            LOG.error("Could not find realm '" + realmTO.getFullPath() + '\'');

            throw new NotFoundException(realmTO.getFullPath());
        }

        Map<Pair<String, String>, Set<Attribute>> beforeAttrs = propagationManager.prepareAttrs(realm);

        PropagationByResource<String> propByRes = binder.update(realm, realmTO);
        realm = realmDAO.save(realm);

        List<PropagationTaskInfo> taskInfos = propagationManager.setAttributeDeltas(
                propagationManager.createTasks(realm, propByRes, null),
                beforeAttrs,
                null);
        PropagationReporter propagationReporter =
                taskExecutor.execute(taskInfos, false, AuthContextUtils.getUsername());

        ProvisioningResult<RealmTO> result = new ProvisioningResult<>();
        result.setEntity(binder.getRealmTO(realm, true));
        result.getPropagationStatuses().addAll(propagationReporter.getStatuses());

        return result;
    }

    @PreAuthorize("hasRole('" + IdRepoEntitlement.REALM_DELETE + "')")
    public ProvisioningResult<RealmTO> delete(final String fullPath) {
        Realm realm = Optional.ofNullable(realmDAO.findByFullPath(fullPath)).
                orElseThrow(() -> new NotFoundException("Realm " + fullPath));

        if (!realmDAO.findChildren(realm).isEmpty()) {
            throw SyncopeClientException.build(ClientExceptionType.RealmContains);
        }

        Set<String> adminRealms = Set.of(realm.getFullPath());
        AnyCond keyCond = new AnyCond(AttrCond.Type.ISNOTNULL);
        keyCond.setSchema("key");
        SearchCond allMatchingCond = SearchCond.getLeaf(keyCond);
        int users = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.USER);
        int groups = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.GROUP);
        int anyObjects = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.ANY_OBJECT);
        int macroTasks = taskDAO.findByRealm(realm).size();
        int clientApps = casSPClientAppDAO.findByRealm(realm).size()
                + saml2SPClientAppDAO.findByRealm(realm).size()
                + oidcRPClientAppDAO.findByRealm(realm).size();

        if (users + groups + anyObjects + macroTasks + clientApps > 0) {
            SyncopeClientException realmContains = SyncopeClientException.build(ClientExceptionType.RealmContains);
            realmContains.getElements().add(users + " user(s)");
            realmContains.getElements().add(groups + " group(s)");
            realmContains.getElements().add(anyObjects + " anyObject(s)");
            realmContains.getElements().add(macroTasks + " command task(s)");
            realmContains.getElements().add(clientApps + " client app(s)");
            throw realmContains;
        }

        PropagationByResource<String> propByRes = new PropagationByResource<>();
        propByRes.addAll(ResourceOperation.DELETE, realm.getResourceKeys());
        List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
        PropagationReporter propagationReporter =
                taskExecutor.execute(taskInfos, false, AuthContextUtils.getUsername());

        ProvisioningResult<RealmTO> result = new ProvisioningResult<>();
        result.setEntity(binder.getRealmTO(realm, true));
        result.getPropagationStatuses().addAll(propagationReporter.getStatuses());

        realmDAO.delete(realm);

        return result;
    }

    @Override
    protected RealmTO resolveReference(final Method method, final Object... args)
            throws UnresolvedReferenceException {

        String fullPath = null;

        if (ArrayUtils.isNotEmpty(args)) {
            for (int i = 0; fullPath == null && i < args.length; i++) {
                if (args[i] instanceof String) {
                    fullPath = (String) args[i];
                } else if (args[i] instanceof RealmTO) {
                    fullPath = ((RealmTO) args[i]).getFullPath();
                }
            }
        }

        if (fullPath != null) {
            try {
                return binder.getRealmTO(realmDAO.findByFullPath(fullPath), true);
            } catch (Throwable e) {
                LOG.debug("Unresolved reference", e);
                throw new UnresolvedReferenceException(e);
            }
        }

        throw new UnresolvedReferenceException();
    }
}
