/*
 * 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.persistence.api.utils;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
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.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ClassUtils;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;

public class RealmUtils {

    public record GroupOwnerRealm(String realmPath, String groupKey) {

        public static Optional<GroupOwnerRealm> of(final String input) {
            String[] split = input.split("@");
            return split == null || split.length < 2
                    ? Optional.empty()
                    : Optional.of(new GroupOwnerRealm(split[0], split[1]));
        }

        public String output() {
            return realmPath + '@' + groupKey;
        }
    }

    public static boolean normalizingAddTo(final Set<String> realms, final String newRealm) {
        boolean dontAdd = false;
        Set<String> toRemove = new HashSet<>();
        for (String realm : realms) {
            if (newRealm.startsWith(realm)) {
                dontAdd = true;
            } else if (realm.startsWith(newRealm)) {
                toRemove.add(realm);
            }
        }

        realms.removeAll(toRemove);
        if (!dontAdd) {
            realms.add(newRealm);
        }
        return !dontAdd;
    }

    public record NormalizedRealms(Set<String> realms, Set<String> groupOwnerRealms) {

        public static NormalizedRealms of(final Collection<String> input) {
            Set<String> realms = new HashSet<>();
            Set<String> groupOwnerRealms = new HashSet<>();
            if (input != null) {
                input.forEach(realm -> {
                    if (realm.indexOf('@') == -1) {
                        normalizingAddTo(realms, realm);
                    } else {
                        groupOwnerRealms.add(realm);
                    }
                });
            }

            return new NormalizedRealms(realms, groupOwnerRealms);
        }
    }

    private static class StartsWithPredicate implements Predicate<String> {

        private final Collection<String> targets;

        StartsWithPredicate(final Collection<String> targets) {
            this.targets = targets;
        }

        @Override
        public boolean test(final String realm) {
            return targets.stream().anyMatch(realm::startsWith);
        }
    }

    private static final Predicate<String> DYN_REALMS_PREDICATE = r -> !r.startsWith(SyncopeConstants.ROOT_REALM);

    public static Set<String> getEffective(final Set<String> allowedRealms, final String requestedRealm) {
        NormalizedRealms normalized = NormalizedRealms.of(allowedRealms);

        Set<String> requested = Set.of(requestedRealm);

        StartsWithPredicate normalizedFilter = new StartsWithPredicate(normalized.realms());
        StartsWithPredicate requestedFilter = new StartsWithPredicate(requested);

        Set<String> effective = new HashSet<>();
        effective.addAll(requested.stream().filter(normalizedFilter).collect(Collectors.toSet()));
        effective.addAll(normalized.realms().stream().filter(requestedFilter).collect(Collectors.toSet()));

        // includes group ownership
        effective.addAll(normalized.groupOwnerRealms());

        // includes dynamic realms
        if (allowedRealms != null) {
            effective.addAll(allowedRealms.stream().filter(DYN_REALMS_PREDICATE).collect(Collectors.toSet()));
        }

        return effective;
    }

    protected static void initFieldNames(final Class<?> entityClass, final Map<String, Field> fields) {
        List<Class<?>> classes = ClassUtils.getAllSuperclasses(entityClass);
        classes.add(entityClass);
        classes.forEach(clazz -> {
            for (Field field : clazz.getDeclaredFields()) {
                if (!Modifier.isStatic(field.getModifiers())
                        && !field.getName().startsWith("pc")
                        && !Collection.class.isAssignableFrom(field.getType())
                        && !Map.class.isAssignableFrom(field.getType())) {

                    fields.put(field.getName(), field);
                    if ("id".equals(field.getName())) {
                        fields.put("key", field);
                    }
                }
            }
        });
    }

    protected final Map<String, Field> fields = new HashMap<>();

    public RealmUtils(final EntityFactory entityFactory) {
        initFieldNames(entityFactory.realmClass(), fields);
    }

    public Optional<Field> getField(final String name) {
        return Optional.ofNullable(fields.get(name));
    }
}
