/*
 *  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.core.metamodel.consent;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.jspecify.annotations.NonNull;

import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.primitives._Ints;
import org.apache.causeway.core.metamodel.facets.object.immutable.ImmutableFacet;

public interface Consent {

    public record VetoReason(
            VetoOriging vetoOriging,
            UiHint uiHint,
            @NonNull String string) implements Serializable {

        /**
         * Introduced to help pick a winner when merging 2 {@link VetoReason}(s).
         * <p>
         * PROGRAMMING_MODEL always wins over SECURITY.
         */
        public enum VetoOriging {
            /**
             * Veto originates from security mechanism.
             * <p>
             * In other words: the user is not authorized to either view or change the feature.
             */
            SECURITY,
            /**
             * Veto originates from programming model, either implicit or explicit see {@link UiHint}.
             */
            PROGRAMMING_MODEL;
            public boolean isSecurity() { return this==SECURITY; }
            public boolean isProgrammingModel() { return this==PROGRAMMING_MODEL; }
            /** reduce to max ordinal */
            static VetoOriging reduceToMaxOrdinal(final VetoOriging a, final VetoOriging b) {
                return b.ordinal()>a.ordinal() ? b : a;
            }
        }
        /**
         * Introduced to help decide whether or not to display a 'ban' icon
         * in the UI, with a tooltip showing the disabled reason.
         */
        public enum UiHint {
            /**
             * Reason is <b>implicit</b> by programming model,
             * reason text is generic and rather of interest to the developer
             * than the end-user.
             * <p>
             * Show only when prototyping. However, when prototyping, can be suppressed via config option.
             */
            NO_ICON_UNLESS_PROTOTYPING,
            /**
             * Reason is <b>explicit</b> either by programming model or authentication mechanism,
             * reason text is designated to reach the end-user.
             */
            SHOW_BAN_ICON;
            public boolean isNoIconUnlessPrototying() { return this==NO_ICON_UNLESS_PROTOTYPING; }
            public boolean isShowBanIcon() { return this==SHOW_BAN_ICON; }
            /** reduce to max ordinal */
            static UiHint reduceToMaxOrdinal(final UiHint a, final UiHint b) {
                return b.ordinal()>a.ordinal() ? b : a;
            }
        }

        private static VetoReason of(
                final @NonNull VetoOriging vetoOriging,
                final @NonNull UiHint uiHint,
                final String reason) {
            _Assert.assertTrue(_Strings.isNotEmpty(reason));
            return new VetoReason(vetoOriging, uiHint, reason);
        }
        public static VetoReason unauthorized(final String reason) {
            return of(VetoOriging.SECURITY, UiHint.SHOW_BAN_ICON, reason);
        }
        public static VetoReason explicit(final String reason) {
            return of(VetoOriging.PROGRAMMING_MODEL, UiHint.SHOW_BAN_ICON, reason);
        }
        private static VetoReason inferred(final String reason) {
            return of(VetoOriging.PROGRAMMING_MODEL, UiHint.NO_ICON_UNLESS_PROTOTYPING, reason);
        }

        VetoReason withAdvisorAsDiagnostic(final Object advisor) {
            return new VetoReason(this.vetoOriging, this.uiHint, this.string + " {" + advisor.getClass().getSimpleName() + "} ");
        }

        public Optional<VetoReason> toOptional() {
            return Optional.of(this);
        }
        /**
         * {@code Pi}: origin=PROGRAMMING_MODEL, nature=implicit (NO_ICON_UNLESS_PROTOTYPING)<br>
         * {@code Pe}: origin=PROGRAMMING_MODEL, nature=explicit (SHOW_BAN_ICON)<br>
         * {@code S(e)}: origin=SECURITY, nature=explicit (SHOW_BAN_ICON)<br>
         * <p>
         * {@code Pi ৹ Pi := pick any or concat}<br>
         * {@code Pe ৹ Pe := pick any or concat}<br>
         * {@code S(e) ৹ S(e) := pick any or concat}<br>
         * <br>
         * {@code Pi ৹ Pe := Pe}<br>
         * {@code Pi ৹ S(e) := Pi}<br>
         * {@code Pe ৹ S(e) := Pe}<br>
         * In other words: winner picking is driven by <i>origin</i> first and <i>nature</i> second.
         */
        public VetoReason reduce(final VetoReason other) {
            // arbitrarily shifting left by 4 so reserving some room for more UiHint enum values
            final int thisScore = (this.vetoOriging.ordinal()<<4)  + this.uiHint.ordinal();
            final int otherScore = (other.vetoOriging.ordinal()<<4)  + other.uiHint.ordinal();

            final List<String> mergedText = new ArrayList<>(2);
            final List<UiHint> winnerUiHint = new ArrayList<>(1);
            switch(_Ints.compare(thisScore, otherScore)) {
            case -1: winnerUiHint.add(other.uiHint); mergedText.add(other.string); break;
            case 0:  winnerUiHint.add(this.uiHint); mergedText.add(this.string); mergedText.add(other.string); break;
            case 1:  winnerUiHint.add(this.uiHint); mergedText.add(this.string); break;
            }
            return new VetoReason(
                    VetoOriging.reduceToMaxOrdinal(this.vetoOriging, other.vetoOriging),
                    winnerUiHint.get(0),
                    mergedText.stream().collect(Collectors.joining("; ")));
        }
        // -- PREDEFINED REASONS
        public static VetoReason editingObjectDisabledReasonNotGiven() {
            return VetoReason.inferred("Disabled via @DomainObject annotation, reason not given.");
        }
        public static VetoReason editingPropertyDisabledReasonNotGiven() {
            return VetoReason.inferred("Disabled via @Property annotation, reason not given.");
        }
        public static VetoReason propertyHasNoSetter() {
            return VetoReason.inferred("Disabled, property has no setter.");
        }
        public static VetoReason bounded() {
            return VetoReason.inferred("Cannot edit a bounded member.");
        }
        public static VetoReason immutableValueType() {
            return VetoReason.inferred("Value types are immutable.");
        }
        public static VetoReason immutablePrimaryKey() {
            return VetoReason.inferred("Primary-keys are immutable.");
        }
        public static VetoReason mixedinCollection() {
            return inferred("Cannot edit a mixed-in collection.");
        }
        public static VetoReason mixedinProperty() {
            return inferred("Cannot edit a mixed-in property.");
        }
        public static VetoReason immutableIfNoReasonGivenByImmutableFacet() {
            return inferred("Immutable, no reason given by ImmutableFacet.");
        }
        public static VetoReason delegatedTo(final @NonNull Class<? extends ImmutableFacet> cls) {
            return inferred("Calculated at runtime, delegating to ImmutableFacet " + cls + ".");
        }
    }

    /**
     * Returns true if this object is giving permission.
     */
    boolean isAllowed();

    /**
     * Returns true if this object is NOT giving permission.
     */
    boolean isVetoed();

    /**
     * Optionally the {@link VetoReason}, why consent is being vetoed, based on whether not allowed.
     * <p>
     * Will correspond to the {@link InteractionResult#getReason() reason} in
     * the contained {@link #getInteractionResult() InteractionResult} (if one
     * was specified).
     */
    Optional<VetoReason> getReason();

    /**
     * Optionally the {@link VetoReason} as String, why consent is being vetoed, based on whether not allowed.
     */
    default Optional<String> getReasonAsString() {
        return getReason().map(VetoReason::string);
    }

    /**
     * Description of the interaction that this consent represents.
     *
     * <p>
     * May be <tt>null</tt>.
     */
    String getDescription();

    /**
     * Allows the description of the interaction to which this consent relates
     * to be specified or refined.
     *
     * @param description
     * @return this consent
     */
    Consent setDescription(String description);

    /**
     * The {@link InteractionResult} that created this {@link Consent}.
     *
     * @return - may be <tt>null</tt> if created as a legacy {@link Consent}.
     *
     */
    public InteractionResult getInteractionResult();

}
