/*
 *  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.runtimeservices.executor;

import java.util.Optional;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;

import org.jspecify.annotations.NonNull;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.command.Command;
import org.apache.causeway.applib.services.iactn.ActionInvocation;
import org.apache.causeway.applib.services.iactn.Execution;
import org.apache.causeway.applib.services.iactn.PropertyEdit;
import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
import org.apache.causeway.applib.services.inject.ServiceInjector;
import org.apache.causeway.applib.services.metrics.MetricsService;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MessageTemplate;
import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.causeway.core.metamodel.execution.ActionExecutor;
import org.apache.causeway.core.metamodel.execution.InteractionInternal;
import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
import org.apache.causeway.core.metamodel.execution.PropertyModifier;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.members.publish.execution.ExecutionPublishingFacet;
import org.apache.causeway.core.metamodel.interactions.InteractionHead;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.core.metamodel.object.MmEntityUtils;
import org.apache.causeway.core.metamodel.object.MmUnwrapUtils;
import org.apache.causeway.core.metamodel.object.MmVisibilityUtils;
import org.apache.causeway.core.metamodel.object.PackedManagedObject;
import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer;
import org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
import org.apache.causeway.core.metamodel.services.ixn.InteractionDtoFactory;
import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
import org.apache.causeway.core.metamodel.services.publishing.ExecutionPublisher;
import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
import org.apache.causeway.schema.ixn.v2.ActionInvocationDto;

import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;

/**
 * Default implementation of {@link MemberExecutorService}.
 *
 * @since 2.0 {@index}
 */
@Service
@Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".MemberExecutorServiceDefault")
@Priority(PriorityPrecedence.EARLY)
@Qualifier("Default")
@RequiredArgsConstructor(onConstructor_ = {@Inject})
@Log4j2
public class MemberExecutorServiceDefault
implements MemberExecutorService {

    private final @Getter InteractionLayerTracker interactionLayerTracker;
    private final @Getter CausewayConfiguration configuration;
    private final @Getter ObjectManager objectManager;
    private final @Getter ClockService clockService;
    private final @Getter DeadlockRecognizer deadlockRecognizer;
    private final @Getter ServiceInjector serviceInjector;
    private final @Getter Provider<MetricsService> metricsServiceProvider;
    private final @Getter InteractionDtoFactory interactionDtoFactory;
    private final @Getter Provider<ExecutionPublisher> executionPublisherProvider;
    private final @Getter MetamodelEventService metamodelEventService;
    private final @Getter TransactionService transactionService;
    private final Provider<CommandPublisher> commandPublisherProvider;

    private MetricsService metricsService() {
        return metricsServiceProvider.get();
    }

    private ExecutionPublisher executionPublisher() {
        return executionPublisherProvider.get();
    }

    @Override
    public Optional<InteractionInternal> getInteraction() {
        return interactionLayerTracker.currentInteraction()
                .map(InteractionInternal.class::cast);
    }

    @Override
    public ManagedObject invokeAction(
            final @NonNull ActionExecutor actionExecutor) {

        var executionResult = actionExecutor.getInteractionInitiatedBy().isPassThrough()
                ? Try.call(()->
                    invokeActionInternally(actionExecutor))
                : getTransactionService().callWithinCurrentTransactionElseCreateNew(()->
                    invokeActionInternally(actionExecutor));

        return executionResult
                .valueAsNullableElseFail();
    }

    private ManagedObject invokeActionInternally(
            final ActionExecutor actionExecutor) {

        final ObjectAction owningAction = actionExecutor.getOwningAction();
        final InteractionHead head = actionExecutor.getHead();
        final Can<ManagedObject> argumentAdapters = actionExecutor.getArguments();
        final InteractionInitiatedBy interactionInitiatedBy = actionExecutor.getInteractionInitiatedBy();
        //            final MethodFacade methodFacade,
        //            final ActionExecutorFactory actionExecutorFactory,
        final FacetHolder facetHolder = actionExecutor.getFacetHolder();

        if(interactionInitiatedBy.isPassThrough()) {
            var resultPojo = invokeMethodPassThrough(actionExecutor.getMethod(), head, argumentAdapters);
            return facetHolder.getObjectManager().adapt(resultPojo);
        }

        var interaction = getInteractionElseFail();

        prepareCommandForPublishing(interaction.getCommand(), head, owningAction, facetHolder);

        var xrayHandle = _Xray.enterActionInvocation(interactionLayerTracker, interaction, owningAction, head, argumentAdapters);

        var actionId = owningAction.getFeatureIdentifier();
        log.debug("about to invoke action {}", actionId);

        var targetAdapter = head.target();
        var targetPojo = MmUnwrapUtils.single(targetAdapter);

        var argumentPojos = argumentAdapters.stream()
                .map(MmUnwrapUtils::single)
                .collect(_Lists.toUnmodifiable());

        var actionInvocation = new ActionInvocation(
                        interaction, actionId, targetPojo, argumentPojos);

        // sets up startedAt and completedAt on the execution, also manages the execution call graph
        interaction.execute(actionExecutor, actionInvocation, new InteractionInternal.Context(clockService, metricsService(), commandPublisherProvider.get(), deadlockRecognizer));

        // handle any exceptions
        var priorExecution = interaction.getPriorExecutionOrThrowIfAnyException(actionInvocation);

        var returnedPojo = priorExecution.getReturned();
        var returnedAdapter = objectManager.adapt(
                returnedPojo, owningAction::getElementType);

        // assert has bookmark, unless non-scalar
        ManagedObjects.asScalarNonEmpty(returnedAdapter)
        .filter(scalarNonEmpty->!scalarNonEmpty.specialization().isOther()) // don't care
        // if its a transient entity, flush the current transaction, so we get an OID
        .filter(scalarNonEmpty->{
            MmEntityUtils.ifHasNoOidThenFlush(scalarNonEmpty);
            return true;
        })
        .ifPresent(scalarNonEmpty->{
            _Assert.assertTrue(scalarNonEmpty.getBookmark().isPresent(), ()->{
                var returnTypeSpec = scalarNonEmpty.objSpec();
                var violation = returnTypeSpec.isEntity()
                        ? MessageTemplate.ACTION_METHOD_RETURNING_TRANSIENT_ENTITY_NOT_ALLOWED
                        : MessageTemplate.ACTION_METHOD_RETURNING_NON_BOOKMARKABLE_OBJECT_NOT_ALLOWED;
                return violation.builder()
                    .addVariablesFor(actionId)
                    .addVariable("returnTypeSpec", returnTypeSpec.toString())
                    .buildMessage();
            });
        });

        // sync DTO with result
        interactionDtoFactory
        .updateResult((ActionInvocationDto)priorExecution.getDto(), owningAction, returnedAdapter);

        // update Command (if required)
        setCommandResultIfEntity(interaction.getCommand(), returnedAdapter);

        // publish (if not a contributed association, query-only mixin)
        if (ExecutionPublishingFacet.isPublishingEnabled(facetHolder)) {
            executionPublisher().publishActionInvocation(priorExecution);
        }

        var result = resultFilteredHonoringVisibility(returnedAdapter, interactionInitiatedBy);
        _Xray.exitInvocation(xrayHandle);
        return result;
    }

    @Override
    public ManagedObject setOrClearProperty(
            final @NonNull PropertyModifier propertyExecutor) {

        var executionResult = propertyExecutor.getInteractionInitiatedBy().isPassThrough()
                ? Try.call(()->
                    setOrClearPropertyInternally(propertyExecutor))
                : getTransactionService()
                    .callWithinCurrentTransactionElseCreateNew(() ->
                        setOrClearPropertyInternally(propertyExecutor));

        return executionResult
                .valueAsNullableElseFail();
    }

    private ManagedObject setOrClearPropertyInternally(
            final @NonNull PropertyModifier propertyModifier) {

        final InteractionHead head = propertyModifier.getHead();

        var domainObject = head.target();

        if(propertyModifier.getInteractionInitiatedBy().isPassThrough()) {
            /* directly access property setter to prevent triggering of domain events
             * or change tracking, eg. when called in the context of serialization */
            propertyModifier.executeClearOrSetWithoutEvents(propertyModifier.getNewValue());
            return domainObject;
        }

        var interaction = getInteractionElseFail();
        var command = interaction.getCommand();
        if( command==null ) return domainObject;

        var owningProperty = propertyModifier.getOwningProperty();

        prepareCommandForPublishing(command, head, owningProperty, propertyModifier.getFacetHolder());

        var xrayHandle = _Xray.enterPropertyEdit(interactionLayerTracker, interaction, owningProperty, head, propertyModifier.getNewValue());

        var propertyId = owningProperty.getFeatureIdentifier();

        var targetManagedObject = head.target();
        var target = MmUnwrapUtils.single(targetManagedObject);
        var argValuePojo = MmUnwrapUtils.single(propertyModifier.getNewValue());

        var propertyEdit = new PropertyEdit(interaction, propertyId, target, argValuePojo);

        // sets up startedAt and completedAt on the execution, also manages the execution call graph
        var targetPojo = interaction.execute(propertyModifier, propertyEdit,
                new InteractionInternal.Context(clockService, metricsService(), commandPublisherProvider.get(), deadlockRecognizer));

        // handle any exceptions
        final Execution<?, ?> priorExecution = interaction.getPriorExecution();

        // TODO: should also sync DTO's 'threw' attribute here...?

        var executionExceptionIfAny = priorExecution.getThrew();
        if(executionExceptionIfAny != null) {
            throw executionExceptionIfAny instanceof RuntimeException
                ? ((RuntimeException)executionExceptionIfAny)
                : new RuntimeException(executionExceptionIfAny);
        }

        // publish (if not a contributed association, query-only mixin)
        if (ExecutionPublishingFacet.isPublishingEnabled(propertyModifier.getFacetHolder())) {
            executionPublisher().publishPropertyEdit(priorExecution);
        }

        var result = getObjectManager().adapt(targetPojo);
        _Xray.exitInvocation(xrayHandle);
        return result;
    }

    // -- HELPER

    @SneakyThrows
    private Object invokeMethodPassThrough(
            final MethodFacade methodFacade,
            final InteractionHead head,
            final Can<ManagedObject> arguments) {

        final Object[] executionParameters = MmUnwrapUtils.multipleAsArray(arguments);
        final Object targetPojo = MmUnwrapUtils.single(head.target());
        return CanonicalInvoker.invoke(methodFacade, targetPojo, executionParameters);
    }

    private void setCommandResultIfEntity(
            final Command command,
            final ManagedObject resultAdapter) {
        if(command.getResult() != null) {
            // don't trample over any existing result, eg subsequent mixins.
            return;
        }
        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) {
            return;
        }
        var entityState = resultAdapter.getEntityState();
        if(!entityState.isPersistable()) {
            return;
        }
        if(entityState.isHollow()
                || entityState.isDetached()) {
            // ensure that any still-to-be-persisted adapters get persisted to DB.
            getTransactionService().flushTransaction();
        }
        // re-evaluate
        if(!resultAdapter.getEntityState().hasOid()) {
            log.warn("was unable to get a bookmark for the command result, "
                    + "which is an entity: {}", resultAdapter);
            return;
        }
        var bookmark = ManagedObjects.bookmarkElseFail(resultAdapter);
        command.updater().setResult(Try.success(bookmark));
    }

    private ManagedObject resultFilteredHonoringVisibility(
            final ManagedObject resultAdapter,
            final InteractionInitiatedBy interactionInitiatedBy) {

        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(resultAdapter)) {
            return resultAdapter;
        }

        if (!getConfiguration().getCore().getMetaModel().isFilterVisibility()
                || resultAdapter instanceof PackedManagedObject) {
            return resultAdapter;
        }

        return MmVisibilityUtils.isVisible(resultAdapter, interactionInitiatedBy)
                ? resultAdapter
                : null;
    }

    /**
     * Will set the command's CommandPublishingPhase to READY,
     * if command and objectMember have a matching member-id
     * and if the facetHolder has a CommandPublishingFacet (has commandPublishing=ENABLED).
     */
    void prepareCommandForPublishing(
            final @NonNull Command command,
            final @NonNull InteractionHead interactionHead,
            final @NonNull ObjectMember objectMember,
            final @NonNull FacetHolder facetHolder) {

        if(interactionHead.isCommandForMember(command, objectMember)
                && isPublishingEnabled(facetHolder)) {
            command.updater().setPublishingPhase(Command.CommandPublishingPhase.READY);
        }

        commandPublisherProvider.get().ready(command);
    }

}
