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

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.syncope.core.persistence.api.dao.AllowedSchemas;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.LinkingMappingItem;
import org.apache.syncope.core.persistence.api.entity.Membership;
import org.apache.syncope.core.persistence.api.entity.VirSchema;
import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
import org.apache.syncope.core.persistence.api.entity.resource.Provision;
import org.apache.syncope.core.provisioning.api.ConnectorFactory;
import org.apache.syncope.core.provisioning.api.VirAttrHandler;
import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheKey;
import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheValue;
import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
@Component
public class VirAttrHandlerImpl implements VirAttrHandler {

    private static final Logger LOG = LoggerFactory.getLogger(VirAttrHandler.class);

    @Autowired
    private ConnectorFactory connFactory;

    @Autowired
    private VirAttrCache virAttrCache;

    @Autowired
    private OutboundMatcher outboundMatcher;

    @Autowired
    private AnyUtilsFactory anyUtilsFactory;

    @Override
    public void setValues(final Any<?> any, final ConnectorObject connObj) {
        if (any == null) {
            LOG.debug("Null any passed, ignoring");
            return;
        }

        AllowedSchemas<VirSchema> schemas =
                anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, VirSchema.class);
        Stream.concat(
                schemas.getForSelf().stream(),
                schemas.getForMemberships().values().stream().flatMap(Set::stream)).forEach(schema -> {

            VirAttrCacheKey cacheKey = new VirAttrCacheKey(any.getType().getKey(), any.getKey(), schema.getKey());

            Attribute attr = connObj.getAttributeByName(schema.getExtAttrName());
            if (attr == null) {
                virAttrCache.expire(cacheKey);
                LOG.debug("Evicted from cache: {}", cacheKey);
            } else {
                VirAttrCacheValue cacheValue = new VirAttrCacheValue(attr.getValue());
                virAttrCache.put(cacheKey, cacheValue);
                LOG.debug("Set in cache: {}={}", cacheKey, cacheValue);
            }
        });
    }

    private Map<VirSchema, List<String>> getValues(final Any<?> any, final Set<VirSchema> schemas) {
        Set<ExternalResource> resources = anyUtilsFactory.getInstance(any).getAllResources(any);

        Map<VirSchema, List<String>> result = new HashMap<>();

        Map<Provision, Set<VirSchema>> toRead = new HashMap<>();

        schemas.stream().filter(schema -> resources.contains(schema.getProvision().getResource())).forEach(schema -> {
            VirAttrCacheKey cacheKey = new VirAttrCacheKey(any.getType().getKey(), any.getKey(), schema.getKey());
            VirAttrCacheValue cacheValue = virAttrCache.get(cacheKey);

            if (cacheValue != null) {
                LOG.debug("Found in cache: {}={}", cacheKey, cacheValue);
                result.put(schema, cacheValue.getValues());
            } else if (schema.getProvision().getAnyType().equals(any.getType())) {
                Set<VirSchema> schemasToRead = toRead.get(schema.getProvision());
                if (schemasToRead == null) {
                    schemasToRead = new HashSet<>();
                    toRead.put(schema.getProvision(), schemasToRead);
                }
                schemasToRead.add(schema);
            }
        });

        toRead.forEach((provision, schemasToRead) -> {
            LOG.debug("About to read from {}: {}", provision, schemasToRead);

            outboundMatcher.match(
                    connFactory.getConnector(provision.getResource()),
                    any,
                    provision,
                    Optional.empty(),
                    schemasToRead.stream().map(VirSchema::asLinkingMappingItem).toArray(LinkingMappingItem[]::new)).
                    forEach(connObj -> schemasToRead.forEach(schema -> {

                Attribute attr = connObj.getAttributeByName(schema.getExtAttrName());
                if (attr != null) {
                    VirAttrCacheKey cacheKey =
                            new VirAttrCacheKey(any.getType().getKey(), any.getKey(), schema.getKey());
                    VirAttrCacheValue cacheValue = virAttrCache.put(cacheKey, new VirAttrCacheValue(attr.getValue()));
                    LOG.debug("Set in cache: {}={}", cacheKey, cacheValue);

                    result.put(schema, cacheValue.getValues());
                }
            }));
        });

        return result;
    }

    @Override
    public List<String> getValues(final Any<?> any, final VirSchema schema) {
        if (!anyUtilsFactory.getInstance(any).dao().
                findAllowedSchemas(any, VirSchema.class).forSelfContains(schema)) {

            LOG.debug("{} not allowed for {}", schema, any);
            return Collections.emptyList();
        }

        List<String> result = getValues(any, Collections.singleton(schema)).get(schema);
        return result == null ? Collections.emptyList() : result;
    }

    @Override
    public List<String> getValues(final Any<?> any, final Membership<?> membership, final VirSchema schema) {
        if (!anyUtilsFactory.getInstance(any).dao().
                findAllowedSchemas(any, VirSchema.class).getForMembership(membership.getRightEnd()).contains(schema)) {

            LOG.debug("{} not allowed for {}", schema, any);
            return Collections.emptyList();
        }

        List<String> result = getValues(any, Collections.singleton(schema)).get(schema);
        return result == null ? Collections.emptyList() : result;
    }

    @Override
    public Map<VirSchema, List<String>> getValues(final Any<?> any) {
        return getValues(
                any,
                anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, VirSchema.class).getForSelf());
    }

    @Override
    public Map<VirSchema, List<String>> getValues(final Any<?> any, final Membership<?> membership) {
        return getValues(
                any,
                anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, VirSchema.class).
                        getForMembership(membership.getRightEnd()));
    }
}
