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

import java.lang.reflect.Method;
import java.util.Optional;

import jakarta.inject.Inject;

import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.query.Criteria;
import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.query.AllInstancesQuery;
import org.apache.causeway.applib.query.Query;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.repository.EntityState;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.config.beans.CausewayBeanMetaData.PersistenceStack;
import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet;
import org.apache.causeway.core.metamodel.facets.object.entity.EntityOrmMetadata;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.services.idstringifier.IdStringifierLookupService;

import lombok.Getter;
import org.jspecify.annotations.NonNull;
import lombok.extern.log4j.Log4j2;

@Log4j2
class JdbcEntityFacet
extends FacetAbstract
implements EntityFacet {

    // self managed injections via constructor
    @Inject private RelationalMappingContext mappingContext;
    @Inject private IdStringifierLookupService idStringifierLookupService;
    @Inject private JdbcAggregateTemplate jdbcAggregateTemplate;

    private final Class<?> entityClass;
    private PrimaryKeyType<?> primaryKeyType;
    private RelationalPersistentEntity<?> persistentEntity;

    @Getter
    private final EntityOrmMetadata ormMetadata;

    protected JdbcEntityFacet(
            final FacetHolder holder,
            final Class<?> entityClass) {
        super(EntityFacet.class, holder, Precedence.HIGH);
        getServiceInjector().injectServicesInto(this);

        this.entityClass = entityClass;
        this.persistentEntity = _MetadataUtil.jdbcEntityMetamodel(mappingContext, entityClass);
        this.ormMetadata = _MetadataUtil.ormMetadataFor(persistentEntity);
        this.primaryKeyType = idStringifierLookupService
            .primaryKeyTypeFor(entityClass, getPrimaryKeyType());
    }

    // -- ENTITY FACET

    @Override
    public PersistenceStack getPersistenceStack() {
        return PersistenceStack.JDBC;
    }

    @Override
    public Optional<String> identifierFor(final @Nullable Object pojo) {
        return primaryKey(pojo)
                .map(primaryKeyType::enstringWithCast);
    }

    @Override
    public Bookmark validateBookmark(final @NonNull Bookmark bookmark) {
        _Assert.assertNotNull(primaryKeyType.destring(bookmark.identifier()));
        return bookmark;
    }

    @Override
    public Optional<Object> fetchByBookmark(final @NonNull Bookmark bookmark) {
        log.debug("fetchEntity; bookmark={}", bookmark);

        var primaryKey = primaryKeyType.destring(bookmark.identifier());
        var entityPojo = jdbcAggregateTemplate.findById(primaryKey, entityClass);

        return Optional.ofNullable(entityPojo);
    }

    private Class<?> getPrimaryKeyType() {
        return getOrmMetadata().primaryKeyClass();
    }

    @Override
    public Can<ManagedObject> fetchByQuery(final Query<?> query) {

        var range = query.getRange();

        if (query instanceof AllInstancesQuery queryFindAllInstances) {

            var queryEntityType = queryFindAllInstances.getResultType();

            // guard against misuse
            _Assert.assertTypeIsInstanceOf(queryEntityType, entityClass);

            var springQuery = org.springframework.data.relational.core.query.Query.query(Criteria.empty());
            if (range.hasOffset()) {
                springQuery = springQuery.offset(range.getStartAsInt());
            }
            if (range.hasLimit()) {
                springQuery = springQuery.limit(range.getLimitAsInt());
            }

            var list = jdbcAggregateTemplate.findAll(springQuery, entityClass);

            var entitySpec = getEntitySpecification();
            return _NullSafe.stream(list)
                            .map(entity -> ManagedObject.adaptSingular(entitySpec, entity))
                            .collect(Can.toCan());

        }

        throw _Exceptions.unsupportedOperation(
                "Support for Query of type %s not implemented.", query.getClass());
    }

    @Override
    public void persist(final Object pojo) {
        if (pojo == null) {
            return; // nothing to do
        }

        // guard against misuse
        _Assert.assertNullableObjectIsInstanceOf(pojo, entityClass);

        log.debug("about to persist entity {}", pojo);

        jdbcAggregateTemplate.save(pojo);
    }

    @SuppressWarnings("unchecked")
    @Override @Nullable
    public <T> T refresh(final @Nullable T pojo) {
        return primaryKey(pojo)
            .<T>map(pkPojo->(T)jdbcAggregateTemplate.findById(pkPojo, entityClass))
            .orElse(pojo);
    }

    @Override
    public void delete(final Object pojo) {
        if(!isEntityPojo(pojo)) return; // nothing to do
        jdbcAggregateTemplate.delete(pojo);
    }

    @Override
    public EntityState getEntityState(final Object pojo) {
        if(!isEntityPojo(pojo)) return EntityState.NOT_PERSISTABLE;
        var primaryKey = primaryKey(pojo);

        return !primaryKey.isPresent()
            ? EntityState.SNAPSHOT_NO_OID
            : jdbcAggregateTemplate.existsById(primaryKey.get(), entityClass)
                ? EntityState.SNAPSHOT
                : EntityState.TRANSIENT_OR_REMOVED;
    }

    @Override
    public Object versionOf(final Object pojo) {
        //TODO[causeway-persistence-jdbc-CAUSEWAY-3849] versionOf
//        if (getEntityState(pojo).isAttached()) {
//            if (pojo instanceof HasVersion) {
//                return ((HasVersion<?>)pojo).getVersion();
//            }
//        }
        log.warn("version support not yet implemented for Spring Data JDBC integration");
        return null;
    }

    @Override
    public boolean isProxyEnhancement(final Method method) {
        return false;
    }

    @Override
    public <T> T detach(final T pojo) {
        // no-op / Spring Data JDBC has no session management
        return pojo;
    }

    // -- HELPER

    // simple guard
    private boolean isEntityPojo(final Object pojo) {
        return pojo != null
                && entityClass.isAssignableFrom(pojo.getClass());
    }

    private Optional<Object> primaryKey(final @Nullable Object pojo) {
        if(!isEntityPojo(pojo)) return Optional.empty();

        var idProperty = persistentEntity.getRequiredIdProperty();
        var propertyAccessor = persistentEntity.getPropertyAccessor(pojo);
        var primaryKeyIfAny = propertyAccessor.getProperty(idProperty);

        return Optional.ofNullable(primaryKeyIfAny);
    }

}
