/*
 *  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.interactions.managed;

import java.util.Optional;
import java.util.function.Function;

import org.jspecify.annotations.NonNull;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Railway;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.metamodel.facets.object.value.ValueFacet;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
import org.apache.causeway.core.metamodel.spec.feature.ObjectMember.AuthorizationException;

public record ActionInteraction(
        String memberId,
        /**
         * optionally the action's metamodel, based on whether was found by actionId
         */
        @NonNull Optional<ObjectAction> objectAction,
        @NonNull InteractionRailway<ManagedAction> railway)
implements MemberInteraction<ManagedAction, ActionInteraction> {

    public enum SemanticConstraint {
        NONE,
        IDEMPOTENT,
        SAFE
    }

    public record Result(
            ManagedAction managedAction,
            Can<ManagedObject> parameterList,
            ManagedObject actionReturnedObject) {
    }

    public static interface ParameterInvalidCallback {
        void onParameterInvalid(ManagedParameter managedParameter, InteractionVeto veto);
    }

    // -- FACTORIES

    public static ActionInteraction start(
            final @NonNull ManagedObject owner,
            final @NonNull String memberId,
            final @NonNull Where where) {
        return startWithMultiselect(owner, memberId, where, Can::empty);
    }

    public static ActionInteraction startWithMultiselect(
            final @NonNull ManagedObject owner,
            final @NonNull String actionId,
            final @NonNull Where where,
            final @NonNull MultiselectChoices multiselectChoices) {

        var managedAction = ManagedAction.lookupActionWithMultiselect(owner, actionId, where, multiselectChoices);

        final InteractionRailway<ManagedAction> railway = managedAction.isPresent()
                ? InteractionRailway.success(managedAction.get())
                : InteractionRailway.veto(InteractionVeto.notFound(Identifier.Type.ACTION, actionId));

        return new ActionInteraction(
                actionId,
                managedAction.map(x->x.getAction()),
                railway);
    }

    /** Supports composite-value-types via mixin (in case detected). */
    public static ActionInteraction startAsBoundToProperty(
            final ManagedProperty associatedWithProperty,
            final String memberId,
            final Where where) {
        var propertyOwner = associatedWithProperty.getOwner();
        var prop = associatedWithProperty.getMetaModel();
        var elementType = prop.getElementType();

        var valueFacet = elementType.isValue()
                ? (ValueFacet<?>) elementType.valueFacet().orElse(null)
                : null;

        if(valueFacet!=null
                && valueFacet.isCompositeValueType()
                //XXX guard against memberId collision,
                // such that if there is a conflict, the conventional member wins
                // (maybe improve programming model so this cannot happen)
                && propertyOwner.objSpec().getAction(memberId, MixedIn.INCLUDED).isEmpty()) {

            var compositeValueNullable = prop.get(propertyOwner);
            var compositeValue =
                    ManagedObjects.nullOrEmptyToDefault(elementType, compositeValueNullable, ()->
                        valueFacet.selectDefaultsProviderForAttribute(prop)
                            .orElseThrow(()->onMissingDefaultsProvider(prop))
                            .getDefaultValue());

            var mixinAction = valueFacet.selectCompositeValueMixinForProperty(associatedWithProperty);
            if(mixinAction.isPresent()) {
                var managedAction = ManagedAction.of(compositeValue, mixinAction.get(), where);
                return ActionInteraction.wrap(managedAction);
            }
        }
        // fallback if not a composite value
        return ActionInteraction.start(propertyOwner, memberId, where);
    }

    /** Supports composite-value-types via mixin (in case detected). */
    public static ActionInteraction startAsBoundToParameter(
            final ParameterNegotiationModel parameterNegotiationModel,
            final int paramIndex,
            final String memberId,
            final Where where) {

        var actionOwner = parameterNegotiationModel.getActionTarget();
        var param = parameterNegotiationModel.getParamModels().getElseFail(paramIndex);
        var elementType = param.getMetaModel().getElementType();

        var valueFacet = elementType.isValue()
                ? (ValueFacet<?>) elementType.valueFacet().orElse(null)
                : null;
        if(valueFacet!=null
                && valueFacet.isCompositeValueType()
                //XXX guard against memberId collision,
                // such that if there is a conflict, the conventional member wins
                // (maybe improve programming model so this cannot happen)
                && actionOwner.objSpec().getAction(memberId, MixedIn.INCLUDED).isEmpty()) {

            var compositeValueNullable = parameterNegotiationModel.getParamValue(paramIndex);
            var compositeValue =
                    ManagedObjects.nullOrEmptyToDefault(elementType, compositeValueNullable, ()->
                        valueFacet.selectDefaultsProviderForAttribute(param.getMetaModel())
                            .orElseThrow(()->onMissingDefaultsProvider(param.getMetaModel()))
                            .getDefaultValue());

            var mixinAction = valueFacet.selectCompositeValueMixinForParameter(parameterNegotiationModel, paramIndex);
            if(mixinAction.isPresent()) {
                var managedAction = ManagedAction.of(compositeValue, mixinAction.get(), where);
                return ActionInteraction.wrap(managedAction);
            }
        }

        // else if not a composite value
        return ActionInteraction.start(actionOwner, memberId, where);
    }

    public static ActionInteraction wrap(final @NonNull ManagedAction managedAction) {
        var action = managedAction.getAction();
        return new ActionInteraction(
                action.getId(),
                Optional.of(action),
                InteractionRailway.success(managedAction));
    }

    public static ActionInteraction empty(final String actionId) {
        return new ActionInteraction(
                actionId,
                Optional.empty(),
                InteractionRailway.veto(InteractionVeto.notFound(Identifier.Type.ACTION, actionId)));
    }

    // -- METHODS

    public ObjectAction getObjectActionElseFail() {
        return objectAction.orElseThrow(()->_Exceptions
                .noSuchElement("could not resolve action by memberId '%s'", memberId));
    }

    public Optional<Identifier> getFeatureIdentifier() {
        return objectAction.map(ObjectAction::getFeatureIdentifier);
    }

    public ActionInteraction checkSemanticConstraint(final @NonNull SemanticConstraint semanticConstraint) {
        railway.update(action -> switch(semanticConstraint) {
                case NONE -> Optional.empty();
                case IDEMPOTENT -> action.getAction().getSemantics().isIdempotentInNature()
                            ? Optional.empty()
                            : Optional.of(InteractionVeto.actionNotIdempotent(action));
                case SAFE -> action.getAction().getSemantics().isSafeInNature()
                            ? Optional.empty()
                            : Optional.of(InteractionVeto.actionNotSafe(action));
            }
        );
        return this;
    }

    public Optional<ParameterNegotiationModel> startParameterNegotiation() {
        return getManagedAction()
            .map(ManagedAction::startParameterNegotiation);
    }

    public Railway<InteractionVeto, ManagedObject> invokeWith(final ParameterNegotiationModel pendingArgs) {
        pendingArgs.activateValidationFeedback();
        var veto = validate(pendingArgs);
        if(veto.isPresent()) return Railway.failure(veto.get());

        var action = railway.getSuccessElseFail();
        var actionResultOrVeto = action.invoke(pendingArgs.getParamValues());
        return actionResultOrVeto;
    }

    public ManagedObject invokeWithRuleChecking(
            final ParameterNegotiationModel pendingArgs) throws AuthorizationException {
        var action = railway.getSuccessElseFail();
        return action.invokeWithRuleChecking(pendingArgs.getParamValues());
    }

    public Optional<InteractionVeto> validate(
            final @NonNull ParameterNegotiationModel pendingArgs) {
        if(railway.isVeto()) return railway.getVeto();

        var validityConsent = pendingArgs.validateParameterSet(); // full validation
        return validityConsent!=null
                && validityConsent.isVetoed()
            ? Optional.of(InteractionVeto.actionParamInvalid(validityConsent))
            : Optional.empty();
    }

    /**
     * @return optionally the ManagedAction based on whether there
     * was no interaction veto within the originating chain
     */
    public Optional<ManagedAction> getManagedAction() {
        return railway.getSuccess();
    }

    /**
     * @return this Interaction's ManagedAction
     * @throws X if there was any interaction veto within the originating chain
     */
    public <X extends Throwable> ManagedAction getManagedActionElseThrow(
            final Function<InteractionVeto, ? extends X> onFailure) throws X {
        return getManagedMemberElseThrow(onFailure);
    }

    public <X extends Throwable> ManagedAction getManagedActionElseFail() {
        return getManagedActionElseThrow(veto->
            _Exceptions.unrecoverable("action vetoed: " + veto.getReason()));
    }

    // -- HELPER

    private static RuntimeException onMissingDefaultsProvider(final ObjectFeature feature) {
        return _Exceptions.unrecoverable("Could not find a DefaultsProvider for ObjectFeature %s",
                feature.getFeatureIdentifier());
    }

}
