/*
 *  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.causeway.extensions.secman.applib.permission.dom;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.DomainObjectLayout;
import org.apache.causeway.applib.annotation.Editing;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Optionality;
import org.apache.causeway.applib.annotation.Parameter;
import org.apache.causeway.applib.annotation.Programmatic;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.services.appfeat.ApplicationFeature;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureRepository;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
import org.apache.causeway.applib.services.appfeat.ApplicationMemberSort;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.extensions.secman.applib.CausewayModuleExtSecmanApplib;
import org.apache.causeway.extensions.secman.applib.role.dom.ApplicationRole;

import lombok.experimental.UtilityClass;

/**
 * Specifies how a particular {@link #getRole() application role} may interact with a specific
 * {@link ApplicationFeature application feature}.
 *
 * <p>
 *     Each permission has a {@link #getRule() rule} and a {@link #getMode() mode}.  The
 *     {@link ApplicationPermissionRule rule} determines whether the permission
 *     {@link ApplicationPermissionRule#ALLOW grants}
 *     access to the feature or {@link ApplicationPermissionRule#VETO veto}es access
 *     to it.  The {@link ApplicationPermissionMode mode} indicates whether
 *     the role can {@link ApplicationPermissionMode#VIEWING view} the feature
 *     or can {@link ApplicationPermissionMode#CHANGING change} the state of the
 *     system using the feature.
 * </p>
 *
 * <p>
 *     For a given permission, there is an interaction between the {@link ApplicationPermissionRule rule} and the
 *     {@link ApplicationPermissionMode mode}:
 * <ul>
 *     <li>for an {@link ApplicationPermissionRule#ALLOW allow}, a
 *     {@link ApplicationPermissionMode#CHANGING usability} allow
 *     implies {@link ApplicationPermissionMode#VIEWING visibility} allow.
 *     </li>
 *     <li>conversely, for a {@link ApplicationPermissionRule#VETO veto},
 *     a {@link ApplicationPermissionMode#VIEWING visibility} veto
 *     implies a {@link ApplicationPermissionMode#CHANGING usability} veto.</li>
 * </ul>
 * </p>
 *
 * @since 2.0 {@index}
 */
@Named(ApplicationPermission.LOGICAL_TYPE_NAME)
@DomainObject
@DomainObjectLayout(
        titleUiEvent = ApplicationPermission.TitleUiEvent.class,
        iconUiEvent = ApplicationPermission.IconUiEvent.class,
        cssClassUiEvent = ApplicationPermission.CssClassUiEvent.class,
        layoutUiEvent = ApplicationPermission.LayoutUiEvent.class
)
public abstract class ApplicationPermission implements Comparable<ApplicationPermission> {

    public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".ApplicationPermission";
    public static final String SCHEMA = CausewayModuleExtSecmanApplib.SCHEMA;
    public static final String TABLE = "ApplicationPermission";

    @UtilityClass
    public static class Nq {
        public static final String FIND_BY_FEATURE = LOGICAL_TYPE_NAME + ".findByFeature";
        public static final String FIND_BY_ROLE = LOGICAL_TYPE_NAME + ".findByRole";
        public static final String FIND_BY_ROLE_RULE_FEATURE = LOGICAL_TYPE_NAME + ".findByRoleAndRuleAndFeature";
        public static final String FIND_BY_ROLE_RULE_FEATURE_FQN = LOGICAL_TYPE_NAME + ".findByRoleAndRuleAndFeatureAndFqn";
        public static final String FIND_BY_USER = LOGICAL_TYPE_NAME + ".findByUser";
        public static final String FIND_BY_ROLE_NAMES = LOGICAL_TYPE_NAME + ".findByRoleNames";
    }

    // -- UI & DOMAIN EVENTS

    public static class TitleUiEvent extends CausewayModuleExtSecmanApplib.TitleUiEvent<ApplicationPermission> { }
    public static class IconUiEvent extends CausewayModuleExtSecmanApplib.IconUiEvent<ApplicationPermission> { }
    public static class CssClassUiEvent extends CausewayModuleExtSecmanApplib.CssClassUiEvent<ApplicationPermission> { }
    public static class LayoutUiEvent extends CausewayModuleExtSecmanApplib.LayoutUiEvent<ApplicationPermission> { }

    public static abstract class PropertyDomainEvent<T> extends CausewayModuleExtSecmanApplib.PropertyDomainEvent<ApplicationPermission, T> {}
    public static abstract class CollectionDomainEvent<T> extends CausewayModuleExtSecmanApplib.CollectionDomainEvent<ApplicationPermission, T> {}

    @Inject transient ApplicationFeatureRepository featureRepository;

    // -- MODEL

    @ObjectSupport public String title() {
        var buf = new StringBuilder();
        buf.append(getRole().getName()).append(":")  // admin:
        .append(" ").append(getRule().toString()) // Allow|Veto
        .append(" ").append(getMode().toString()) // Viewing|Changing
        .append(" of ");

        asFeatureId()
        .ifPresent(featureId->{

            switch (featureId.getSort()) {
            case NAMESPACE:
                buf.append(getFeatureFqn());              // com.mycompany
                break;
            case TYPE:
                // abbreviate if required because otherwise title overflows on action prompt.
                if(getFeatureFqn().length() < 30) {
                    buf.append(getFeatureFqn());          // com.mycompany.Bar
                } else {
                    buf.append(featureId.getTypeSimpleName()); // Bar
                }
                break;
            case MEMBER:
                buf.append(featureId.getTypeSimpleName())
                .append("#")
                .append(featureId.getLogicalMemberName());   // com.mycompany.Bar#foo
                break;
            }

        });

        return buf.toString();
    }

    // -- ROLE

    @Property(
            domainEvent = Role.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.MANDATORY
    )
    @PropertyLayout(
            fieldSetId = "identity",
            hidden = Where.REFERENCES_PARENT,
            sequence = "1"
    )
    @Parameter(
            optionality = Optionality.MANDATORY
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Role {
        class DomainEvent extends PropertyDomainEvent<ApplicationRole> {}
        String NAME = "roleId";
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }

    @Role
    public abstract ApplicationRole getRole();
    public abstract void setRole(ApplicationRole applicationRole);

    // -- RULE

    @Property(
            domainEvent = Rule.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.MANDATORY
    )
    @PropertyLayout(
            fieldSetId = "rule",
            sequence = "1"
    )
    @Parameter(
            optionality = Optionality.MANDATORY
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Rule {
        class DomainEvent extends PropertyDomainEvent<ApplicationPermissionRule> {}
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    @Rule
    public abstract ApplicationPermissionRule getRule();
    public abstract void setRule(ApplicationPermissionRule rule);

    // -- MODE

    @Property(
            domainEvent = Mode.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.MANDATORY
    )
    @PropertyLayout(
            fieldSetId = "mode",
            sequence = "1"
    )
    @Parameter(
            optionality = Optionality.MANDATORY
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Mode {
        class DomainEvent extends PropertyDomainEvent<ApplicationPermissionMode> {}
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    @Mode
    public abstract ApplicationPermissionMode getMode();
    public abstract void setMode(ApplicationPermissionMode mode);

    // -- SORT

    /**
     * Combines {@link #getFeatureSort() feature type} and member type.
     */
    @Property(
            domainEvent = Sort.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "identity",
            sequence = "3",
            typicalLength= Sort.TYPICAL_LENGTH
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Sort {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int TYPICAL_LENGTH = 7;  // ApplicationFeatureType.PACKAGE is longest
    }
    @Sort
    public String getSort() {
        final Enum<?> e = getFeatureSort() != ApplicationFeatureSort.MEMBER
                ? getFeatureSort()
                : getMemberSort().orElse(null);
        return e != null ? e.name(): null;
    }

    // -- FEATURE SORT

    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeatureSort {
        int TYPICAL_LENGTH = 7;  // ApplicationFeatureType.PACKAGE is longest
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    /**
     * Which {@link ApplicationFeatureId#getSort() sort} of
     * feature this is.
     *
     * <p>
     *     The combination of the feature type and the {@link #getFeatureFqn() fully qualified name} is used to build
     *     the corresponding {@link #getFeature() feature} (view model).
     * </p>
     *
     * @see #getFeatureFqn()
     */
    @Programmatic
    public abstract ApplicationFeatureSort getFeatureSort();
    public abstract void setFeatureSort(ApplicationFeatureSort featureSort);

    // -- FQN

    /**
     * The {@link ApplicationFeatureId#getFullyQualifiedName() fully qualified name}
     * of the feature.
     *
     * <p>
     *     The combination of the {@link #getFeatureSort() feature type} and the fully qualified name is used to build
     *     the corresponding feature view model.
     * </p>
     *
     * @see #getFeatureSort()
     */
    @Property(
            domainEvent = FeatureFqn.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "identity",
            sequence = "2"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeatureFqn {
        class DomainEvent extends PropertyDomainEvent<String> {}
        boolean NULLABLE = false;
        String ALLOWS_NULL = "false";
    }
    @FeatureFqn
    public abstract String getFeatureFqn();
    public abstract void setFeatureFqn(String featureFqn);

    // -- FIND FEATURE

    @Programmatic public ApplicationFeature findFeature(final ApplicationFeatureId featureId) {
        return featureRepository.findFeature(featureId);
    }

    @Programmatic private Optional<ApplicationMemberSort> getMemberSort() {
        return getFeature()
                .flatMap(ApplicationFeature::getMemberSort);
    }

    @Programmatic private Optional<ApplicationFeature> getFeature() {
        return asFeatureId()
                .map(this::findFeature);
    }

    // -- HELPER

    @Programmatic Optional<ApplicationFeatureId> asFeatureId() {
        return Optional.ofNullable(getFeatureSort())
                .map(featureSort -> ApplicationFeatureId.newFeature(featureSort, getFeatureFqn()));
    }

    // -- CONTRACT

    private static final ObjectContracts.ObjectContract<ApplicationPermission> contract	=
            ObjectContracts.contract(ApplicationPermission.class)
                    .thenUse("role", ApplicationPermission::getRole)
                    .thenUse("featureSort", ApplicationPermission::getFeatureSort)
                    .thenUse("featureFqn", ApplicationPermission::getFeatureFqn)
                    .thenUse("mode", ApplicationPermission::getMode);

    @Override
    public int compareTo(final org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission other) {
        return contract.compare(this, other);
    }

    @Override
    public boolean equals(final Object other) {
        return contract.equals(this, other);
    }

    @Override
    public int hashCode() {
        return contract.hashCode(this);
    }

    @Override
    public String toString() {
        return contract.toString(this);
    }

    public static class DefaultComparator implements Comparator<ApplicationPermission> {
        @Override
        public int compare(final ApplicationPermission o1, final ApplicationPermission o2) {
            return Objects.compare(o1, o2, Comparator.naturalOrder());
        }
    }

    @UtilityClass
    public static final class Functions {
        public static final Function<ApplicationPermission, ApplicationPermissionValue> AS_VALUE =
                input -> new ApplicationPermissionValue(
                        input.asFeatureId().orElseThrow(_Exceptions::noSuchElement),
                        input.getRule(),
                        input.getMode());
    }

}
