package org.apereo.cas.authentication.principal.cache;

import org.apereo.cas.authentication.attribute.PrincipalAttributeRepositoryFetcher;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.RegisteredServicePrincipalAttributesRepository;
import org.apereo.cas.configuration.model.core.authentication.PrincipalAttributesCoreProperties;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.util.CollectionUtils;
import org.apereo.cas.util.spring.ApplicationContextProvider;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.ObjectUtils;
import org.apereo.services.persondir.IPersonAttributeDao;

import jakarta.persistence.Transient;

import java.io.Serial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * Parent class for retrieval principals attributes, provides operations
 * around caching, merging of attributes.
 *
 * @author Misagh Moayyed
 * @since 4.2
 */
@Slf4j
@ToString(exclude = "lock")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = {"mergingStrategy", "attributeRepositoryIds"})
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public abstract class AbstractPrincipalAttributesRepository implements RegisteredServicePrincipalAttributesRepository, AutoCloseable {
    @Serial
    private static final long serialVersionUID = 6350245643948535906L;

    @JsonIgnore
    @Transient
    @org.springframework.data.annotation.Transient
    private final transient Object lock = new Object();

    /**
     * The merging strategy that deals with existing principal attributes
     * and those that are retrieved from the source. By default, existing attributes
     * are ignored and the source is always consulted.
     */
    @Getter
    @Setter
    private PrincipalAttributesCoreProperties.MergingStrategyTypes mergingStrategy =
        PrincipalAttributesCoreProperties.MergingStrategyTypes.MULTIVALUED;

    @Getter
    @Setter
    private Set<String> attributeRepositoryIds = new LinkedHashSet<>(0);

    @Getter
    @Setter
    private boolean ignoreResolvedAttributes;

    /**
     * Gets attribute repository.
     *
     * @return the attribute repository
     */
    @JsonIgnore
    protected static IPersonAttributeDao getAttributeRepository() {
        val repositories = ApplicationContextProvider.getAttributeRepository();
        return repositories.orElse(null);
    }

    /***
     * Convert principal attributes to person attributes.
     * @param attributes the attributes
     * @return person attributes
     */
    protected static Map<String, List<Object>> convertPrincipalAttributesToPersonAttributes(final Map<String, ?> attributes) {
        val convertedAttributes = new TreeMap<String, List<Object>>(String.CASE_INSENSITIVE_ORDER);
        val principalAttributes = new LinkedHashMap<>(attributes);
        LOGGER.trace("Principal attributes to convert to person attributes are [{}]", principalAttributes);
        principalAttributes.forEach((key, values) -> {
            if (values instanceof final Collection collection) {
                val uniqueValues = new LinkedHashSet<Object>(collection);
                val listedValues = new ArrayList<Object>(uniqueValues);
                convertedAttributes.put(key, listedValues);
            } else {
                convertedAttributes.put(key, CollectionUtils.wrap(values));
            }
        });
        LOGGER.trace("Converted principal attributes, now as person attributes are [{}]", convertedAttributes);
        return convertedAttributes;
    }

    /**
     * Convert person attributes to principal attributes.
     *
     * @param attributes person attributes
     * @return principal attributes
     */
    protected static Map<String, List<Object>> convertPersonAttributesToPrincipalAttributes(final Map<String, List<Object>> attributes) {
        return attributes.entrySet()
            .stream()
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    @Override
    public void close() {
    }

    /**
     * Convert attributes to principal attributes and cache.
     *
     * @param principal         the principal
     * @param sourceAttributes  the source attributes
     * @param registeredService the registered service
     * @return the map
     */
    protected Map<String, List<Object>> convertAttributesToPrincipalAttributesAndCache(final Principal principal,
                                                                                       final Map<String, List<Object>> sourceAttributes,
                                                                                       final RegisteredService registeredService) {
        val finalAttributes = convertPersonAttributesToPrincipalAttributes(sourceAttributes);
        update(principal.getId(), finalAttributes, registeredService);
        LOGGER.trace("Final principal attributes after caching, if any, are [{}]", finalAttributes);
        return finalAttributes;
    }

    /**
     * Calculate merging strategy attribute merging strategy.
     *
     * @return the attribute merging strategy
     */
    protected PrincipalAttributesCoreProperties.MergingStrategyTypes determineMergingStrategy() {
        return ObjectUtils.defaultIfNull(getMergingStrategy(), PrincipalAttributesCoreProperties.MergingStrategyTypes.MULTIVALUED);
    }

    /**
     * Are attribute repository ids defined boolean.
     *
     * @return true/false
     */
    @JsonIgnore
    protected boolean areAttributeRepositoryIdsDefined() {
        return attributeRepositoryIds != null && !attributeRepositoryIds.isEmpty();
    }

    /**
     * Obtains attributes first from the repository by calling
     * {@link IPersonAttributeDao#getPerson(String)}.
     *
     * @param principal the person to locate in the attribute repository
     * @return the map of attributes
     */
    protected Map<String, List<Object>> retrievePersonAttributesFromAttributeRepository(final Principal principal) {
        synchronized (lock) {
            val repository = getAttributeRepository();
            if (repository == null) {
                LOGGER.warn("No attribute repositories could be fetched from application context");
                return new HashMap<>(0);
            }

            return PrincipalAttributeRepositoryFetcher.builder()
                .attributeRepository(repository)
                .principalId(principal.getId())
                .activeAttributeRepositoryIdentifiers(this.attributeRepositoryIds)
                .currentPrincipal(principal)
                .build()
                .retrieve();
        }
    }

    /**
     * Gets principal attributes.
     *
     * @param principal the principal
     * @return the principal attributes
     */
    @JsonIgnore
    protected Map<String, List<Object>> getPrincipalAttributes(final Principal principal) {
        if (ignoreResolvedAttributes) {
            return new HashMap<>(0);
        }
        return convertPrincipalAttributesToPersonAttributes(principal.getAttributes());
    }
}
