/*
 *  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.facets.object.entity;

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

import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;

import org.apache.causeway.applib.query.Query;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.IdStringifier;
import org.apache.causeway.applib.services.repository.EntityState;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.core.config.beans.CausewayBeanMetaData.PersistenceStack;
import org.apache.causeway.core.metamodel.facetapi.Facet;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.object.MmSpecUtils;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;

import org.jspecify.annotations.NonNull;

/**
 * Indicates that this class is managed by a persistence context.
 * @since 2.0
 */
public interface EntityFacet extends Facet {

    record PrimaryKeyType<T>(
            @NonNull Class<?> owningEntityClass,
            @NonNull IdStringifier<T> idStringifier,
            @NonNull Class<T> primaryKeyClass) {

        public String enstring(final T primaryKey) {
            return idStringifier.enstring(primaryKey);
        }
        public String enstringWithCast(final Object primaryKey) {
            return _Casts.castTo(primaryKeyClass, primaryKey)
                .map(idStringifier::enstring)
                .orElseThrow(()->_Exceptions.illegalArgument(
                        "failed to cast primary-key '%s' to expected type %s",
                            ""+primaryKey,
                            primaryKeyClass.getName()));
        }
        public T destring(final String stringifiedPrimaryKey) {
            return idStringifier.destring(owningEntityClass, stringifiedPrimaryKey);
        }
        /** shallow PK detection */
        public boolean isValid(final @NonNull Object primaryKey) {
            return _Casts.castTo(primaryKeyClass, primaryKey)
                    .map(idStringifier::isValid)
                    .orElse(false);
        }
        public static <T> PrimaryKeyType<T> getInstance(
                final @NonNull Class<?> owningEntityClass,
                final @NonNull Function<Class<T>, IdStringifier<T>> stringifierLookup,
                final @NonNull Class<T> primaryKeyClass){
            return new PrimaryKeyType<>(
                    owningEntityClass,
                    stringifierLookup.apply(primaryKeyClass),
                    _Casts.uncheckedCast(ClassUtils.resolvePrimitiveIfNecessary(primaryKeyClass)));
        }
    }

    /**
     * The {@link ObjectSpecification} of the entity type this
     * facet is associated with.
     */
    default ObjectSpecification getEntitySpecification() {
        return (ObjectSpecification)getFacetHolder();
    }

    /**
     * Introduced purely for optimization purposes.
     * @implNote if possible memoizes the fact as to whether
     *      services were already injected into given pojo,
     *      and if so allows to skip any consecutive injection attempts
     */
    default boolean isInjectionPointsResolved(final @Nullable Object pojo) {
        return pojo==null;
    }

    /**
     * Optionally the stringified OID,
     * based on whether the entity has one associated.
     * @throws IllegalArgumentException if the pojo's class is not recognized
     *      by the persistence layer
     */
    Optional<String> identifierFor(@Nullable Object pojo);

    Bookmark validateBookmark(@NonNull Bookmark bookmark);

    /**
     * Optionally the {@link Bookmark},
     * based on whether the entity has an OID associated.
     * eg. it has not if not persisted yet
     * @throws IllegalArgumentException if the pojo's class is not recognized
     *      by the persistence layer or does not exactly match the expected
     */
    default Optional<Bookmark> bookmarkFor(final @Nullable Object pojo) {
        return identifierFor(pojo)
                .map(id->Bookmark.forLogicalTypeAndIdentifier(
                        MmSpecUtils.quicklyResolveObjectSpecification(
                                getEntitySpecification(),
                                pojo.getClass())
                        .logicalType(),
                        id));
    }

    default Bookmark bookmarkForElseFail(final @Nullable Object pojo) {
        return bookmarkFor(pojo)
                .orElseThrow(()->_Exceptions.noSuchElement("entity has no OID: %s",
                        getEntitySpecification().logicalType()));
    }

    /**
     * Optionally the entity pojo corresponding to given {@link Bookmark},
     * based on whether could be found.
     */
    Optional<Object> fetchByBookmark(Bookmark bookmark);

    Can<ManagedObject> fetchByQuery(Query<?> query);

    void persist(Object pojo);

    @Nullable <T> T refresh(@Nullable T pojo);

    void delete(Object pojo);

    EntityState getEntityState(Object pojo);

    Object versionOf(Object pojo);

    /**
     * Whether given method originates from byte code mangling.
     * @param method
     */
    boolean isProxyEnhancement(Method method);

    @Nullable <T> T detach(@Nullable T pojo);

    PersistenceStack getPersistenceStack();

    /**
     * Provides associated table and column names.
     */
    EntityOrmMetadata getOrmMetadata();

    // -- JUNIT SUPPORT

    static EntityFacet forTesting(
            final PersistenceStack persistenceStandard,
            final FacetHolder facetHolder) {
        return new _EntityFacetForTesting(persistenceStandard, facetHolder);
    }

}
