/*
 *  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.persistence.jpa.applib.integration;

import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreRemove;
import jakarta.persistence.PreUpdate;

import org.eclipse.persistence.sessions.UnitOfWork;
import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
import org.eclipse.persistence.sessions.changesets.ObjectChangeSet;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Either;
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.objectmanager.ObjectManager;
import org.apache.causeway.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
import org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
import org.apache.causeway.persistence.jpa.applib.services.JpaSupportService;

import lombok.extern.log4j.Log4j2;

/**
 * EntityListener class for listing with the {@link jakarta.persistence.EntityListeners} annotation, to
 * support injection point resolving for entities, and to notify {@link ObjectLifecyclePublisher} of changes.
 *
 * <p>
 * Instances of this class are not managed by Spring, but by the persistence layer.
 * </p>
 *
 * <p>
 * The particular persistence layer implementation in use needs to be configured,
 * with a BeanManager, that is able to resolve injection points for this EntityListener.
 * </p>
 *
 * @since 2.0 {@index}
 */
@Log4j2
public class CausewayEntityListener {

    // injection points resolved via BeanManagerForEntityListeners ...
    @Inject private ObjectLifecyclePublisher objectLifecyclePublisher;
    @Inject private Provider<JpaSupportService> jpaSupportServiceProvider;
    @Inject private ObjectManager objectManager;

    @PrePersist void onPrePersist(final Object entityPojo) {
        log.debug("onPrePersist: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);
        objectLifecyclePublisher.onPrePersist(Either.left(entity));
    }

    @PostLoad void onPostLoad(final Object entityPojo) {
        log.debug("onPostLoad: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);

        var entityState = entity.getEntityState();
        if(!entityState.isAttached()) {
            // [CAUSEWAY-3265] seeing this with JPA
            // if we don't exit here will cause a nested loop repeatedly trying to refetch the pojo
            log.error("onPostLoad event while pojo not attached ({}); ignoring the event",
                    entityState.name());
            return;
        }
        objectLifecyclePublisher.onPostLoad(entity);
    }

    @PreUpdate void onPreUpdate(final Object entityPojo) {
        log.debug("onPreUpdate: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);
        objectLifecyclePublisher.onPreUpdate(entity, this::gatherPropertyChangeRecords);
    }

    @PreRemove void onPreRemove(final Object entityPojo) {
        log.debug("onAnyRemove: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);
        objectLifecyclePublisher.onPreRemove(entity);
    }

    @PostPersist void onPostPersist(final Object entityPojo) {
        log.debug("onPostPersist: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);
        objectLifecyclePublisher.onPostPersist(entity);
    }

    @PostUpdate void onPostUpdate(final Object entityPojo) {
        log.debug("onPostUpdate: {}", entityPojo);
        var entity = objectManager.adapt(entityPojo);
        objectLifecyclePublisher.onPostUpdate(entity);
    }

    @PostRemove void onPostRemove(final Object entityPojo) {
        log.debug("onPostRemove: {}", entityPojo);
    }

    // -- HELPER

    private Can<PropertyChangeRecord> gatherPropertyChangeRecords(final @Nullable ManagedObject entity) {

        // guard against null and non-entity types
        if(!ManagedObjects.isEntity(entity)) {
            return Can.empty();
        }

        final Object entityPojo = entity.getPojo();
        var entityManagerIfAny = jpaSupportServiceProvider.get().getEntityManager(entityPojo.getClass())
                .getValue();

        final Can<PropertyChangeRecord> propertyChangeRecords = entityManagerIfAny
            .map(em -> {
                // https://wiki.eclipse.org/EclipseLink/FAQ/JPA#How_to_access_what_changed_in_an_object_or_transaction.3F
                var unwrap = em.unwrap(UnitOfWork.class);
                var changes = unwrap.getCurrentChanges();
                return changes.getObjectChangeSetForClone(entityPojo);
            })
            .map((final ObjectChangeSet objectChanges)->{
                return
                    objectChanges.getChanges().stream()
                    .filter(DirectToFieldChangeRecord.class::isInstance)
                    .map(DirectToFieldChangeRecord.class::cast)
                    .map(ormChangeRecord -> {
                        final String propertyName = ormChangeRecord.getAttribute();
                        return MmEntityUtils.lookupPropertyChangeRecordIdForChangePublishing(entity, propertyName)
                                .map(id -> PropertyChangeRecord.ofCurrent(id, ormChangeRecord.getOldValue()))
                                .orElse(null); // ignore
                    })
                    .collect(Can.toCan()); // a Can only collects non-null elements
            })
            .orElseGet(Can::empty);

        return propertyChangeRecords;
    }

}
