package de.tudarmstadt.hrz.idm.oss.custom.persistance.jpa;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.apache.syncope.core.persistence.api.dao.search.AbstractSearchCond;
import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Matching group membership
 */
public class GroupMembershipMatcher {

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

    private enum MatchResult {
        NEW,
        IN_PROGRESS,
        INDEFINITE,
        MATCH,
        NO_MATCH
    }

    private static final class MatchResultHolder {

        public MatchResult matchResult = MatchResult.NEW;

        public static final Function<String, MatchResultHolder> GENERATOR = group -> new MatchResultHolder();
    }

    private final Map<String, MatchResultHolder> uDynGroupMembershipMatches = new HashMap<>();

    private final Function<String, String> groupKeyToFiqlCond;

    private final Function<String, SearchCond> buildDynMembershipCond;

    private final Function<String, List<String>> groupExpressionResolver;

    private final Function<SearchCond, Boolean> searchCondMatcher;

    private final Function<String, Boolean> directGroupMatcher;

    /**
     * Create a matcher for group membership
     *
     * @param groupKeyToFiqlCond      returns a given group's FIQL expression for dynamic
     *                                membership, or {@code null} if there is none (meaning the
     *                                group can contain only static members)
     * @param buildDynMembershipCond  returns the parsed condition of a given FIQL expression
     * @param groupExpressionResolver returns the keys of groups that match a given wildcard
     *                                expression
     * @param searchCondMatcher       returns whether a given condition matches
     * @param directGroupMatcher      returns whether there is a direct membership in a given group
     */
    public GroupMembershipMatcher(
        final Function<String, String> groupKeyToFiqlCond,
        final Function<String, SearchCond> buildDynMembershipCond,
        final Function<String, List<String>> groupExpressionResolver,
        final Function<SearchCond, Boolean> searchCondMatcher,
        final Function<String, Boolean> directGroupMatcher
    ) {
        this.groupKeyToFiqlCond = groupKeyToFiqlCond;
        this.buildDynMembershipCond = buildDynMembershipCond;
        this.groupExpressionResolver = groupExpressionResolver;
        this.searchCondMatcher = searchCondMatcher;
        this.directGroupMatcher = directGroupMatcher;
    }

    private Object matchesAbstractSearchCond(final AbstractSearchCond searchCond) {
        if (!(searchCond instanceof MembershipCond)) {
            // we only handle memberships, so exit early
            if (searchCond instanceof SearchCond) {
                // this could be an expression tree
                return matchesSearchCond((SearchCond) searchCond);
            }
            return searchCond;
        }
        final String groupExp = ((MembershipCond) searchCond).getGroup();
        final List<String> groupKeys = groupExpressionResolver.apply(groupExp);
        for (final String groupKey : groupKeys) {
            final Object result = matches(groupKey);
            if (FALSE != result) {
                return result;
            }
        }
        return FALSE;
    }

    private Object matchesSearchCond(final SearchCond searchCond) {
        return switch (searchCond.getType()) {
            case LEAF -> {
                final Object result = matchesAbstractSearchCond(
                    searchCond.getLeaf(AbstractSearchCond.class).get()
                );
                if (result instanceof AbstractSearchCond) {
                    yield SearchCond.getLeaf((AbstractSearchCond) result);
                } else {
                    yield result;
                }
            }
            case NOT_LEAF -> {
                final Object result = matchesAbstractSearchCond(
                    searchCond.getLeaf(AbstractSearchCond.class).get()
                );
                if (result instanceof AbstractSearchCond) {
                    yield SearchCond.getNotLeaf((AbstractSearchCond) result);
                } else {
                    if (null == result) {
                        yield null;
                    } else {
                        yield !(Boolean) result;
                    }
                }
            }
            case AND -> {
                final Object left = matchesSearchCond(searchCond.getLeft());
                if (left instanceof SearchCond) {
                    final Object right = matchesSearchCond(searchCond.getRight());
                    if (right instanceof SearchCond) {
                        yield SearchCond.getAnd(List.of((SearchCond) left, (SearchCond) right));
                    } else {
                        if (TRUE == right) {
                            yield left;
                        } else {
                            yield right;
                        }
                    }
                } else {
                    if (TRUE == left) {
                        yield matchesSearchCond(searchCond.getRight());
                    } else {
                        yield left;
                    }
                }
            }
            case OR -> {
                final Object left = matchesSearchCond(searchCond.getLeft());
                if (left instanceof SearchCond) {
                    final Object right = matchesSearchCond(searchCond.getRight());
                    if (right instanceof SearchCond) {
                        yield SearchCond.getOr(List.of((SearchCond) left, (SearchCond) right));
                    } else {
                        if (FALSE == right) {
                            yield left;
                        } else {
                            yield right;
                        }
                    }
                } else {
                    if (FALSE == left) {
                        yield matchesSearchCond(searchCond.getRight());
                    } else {
                        yield left;
                    }
                }
            }
        };
    }

    /**
     * Check for membership in a group
     *
     * @param groupKey the key of the group
     * @return {@code null} if there is no definite result (e.g. if dynamic membership conditions
     * form circular dependencies), else the definite result
     */
    public Boolean matches(final String groupKey) {
        final String fiqlCond = groupKeyToFiqlCond.apply(groupKey);
        if (null == fiqlCond) {
            // no dynamic group membership
            return (directGroupMatcher.apply(groupKey));
        }
        final MatchResultHolder matchResultHolder = uDynGroupMembershipMatches.computeIfAbsent(
            groupKey,
            MatchResultHolder.GENERATOR
        );
        final MatchResult matchResult = matchResultHolder.matchResult;
        switch (matchResult) {
            case NEW -> {
                // first time
            }
            case IN_PROGRESS -> {
                // we have been here before
                LOG.error(
                    "Circular dynamic group membership condition for {}",
                    groupKey
                );
                matchResultHolder.matchResult = MatchResult.INDEFINITE;
                return null;
            }
            case INDEFINITE -> {
                return null;
            }
            case MATCH -> {
                return TRUE;
            }
            case NO_MATCH -> {
                return FALSE;
            }
        }
        if (directGroupMatcher.apply(groupKey)) {
            // we are a direct group member; exit early
            matchResultHolder.matchResult = MatchResult.MATCH;
            return true;
        }
        matchResultHolder.matchResult = MatchResult.IN_PROGRESS;
        final SearchCond cond = buildDynMembershipCond.apply(fiqlCond);
        final Object result = matchesSearchCond(cond);
        final Boolean rc;
        if (result instanceof SearchCond) {
            rc = searchCondMatcher.apply((SearchCond) result);
        } else {
            rc = ((Boolean) result);
        }
        if (null == rc) {
            matchResultHolder.matchResult = MatchResult.INDEFINITE;
        } else {
            matchResultHolder.matchResult = (rc) ? MatchResult.MATCH : MatchResult.NO_MATCH;
        }
        return rc;
    }
}
