/*
 *  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
 *
 *    https://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.grails.orm.hibernate.cfg.domainbinding.hibernate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.annotation.Nonnull;

import org.hibernate.FetchMode;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.SimpleValue;

import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.model.PersistentProperty;
import org.grails.datastore.mapping.model.config.GormProperties;
import org.grails.orm.hibernate.cfg.DiscriminatorConfig;
import org.grails.orm.hibernate.cfg.HibernateCompositeIdentity;
import org.grails.orm.hibernate.cfg.HibernateSimpleIdentity;
import org.grails.orm.hibernate.cfg.Mapping;
import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
import org.grails.orm.hibernate.cfg.domainbinding.util.ConfigureDerivedPropertiesConsumer;
import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher;
import org.grails.orm.hibernate.cfg.domainbinding.util.NamespaceNameExtractor;

import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.JPA_DEFAULT_DISCRIMINATOR_TYPE;

/** Common interface for Hibernate persistent entities */
public interface GrailsHibernatePersistentEntity extends PersistentEntity {

    private static String resolveDiscriminatorValue(DiscriminatorConfig discriminatorConfig) {
        return discriminatorConfig.getColumn() != null ?
                discriminatorConfig.getColumn().getName() :
                discriminatorConfig.getFormula();
    }

    @Override
    Mapping getMappedForm();

    @Nonnull
    default GrailsHibernatePersistentEntity getHibernateRootEntity() {
        return (GrailsHibernatePersistentEntity) getRootEntity();
    }

    default GrailsHibernatePersistentEntity getStrategyOwner() {
        List<HibernatePersistentProperty> props = getHibernatePersistentProperties();
        return (props != null && !props.isEmpty()) ? props.get(0).getHibernateOwner() : this;
    }

    default Mapping getStrategyMapping() {
        return getStrategyOwner().getMappedForm();
    }

    default Mapping getRootMapping() {
        return getHibernateRootEntity().getMappedForm();
    }

    default boolean isTablePerHierarchy() {
        Mapping mapping = getStrategyMapping();
        return mapping == null || mapping.isTablePerHierarchy();
    }

    default boolean isJoinedSubclass() {
        Mapping mapping = getStrategyMapping();
        return mapping != null && mapping.isJoinedSubclass();
    }

    default boolean isUnionSubclass() {
        Mapping mapping = getStrategyMapping();
        return mapping != null && mapping.isUnionSubclass();
    }

    default boolean isTableAbstract() {
        return isUnionSubclass() && isAbstract();
    }

    default boolean isTablePerHierarchySubclass() {
        return !this.isRoot() && isTablePerHierarchy();
    }

    default Set<String> buildDiscriminatorSet() {
        String quote = Optional.ofNullable(getRootMapping())
                .filter(m -> m.getDatasources() != null)
                .map(Mapping::getDiscriminator)
                .filter(config -> config.getType() != null && !config.getType().equals("string"))
                .map(config -> "")
                .orElse("'");

        String quotedDiscriminator = quote + getDiscriminatorValue() + quote;

        return Stream.concat(
                        Stream.of(quotedDiscriminator),
                        getChildEntities().stream()
                                .map(GrailsHibernatePersistentEntity::buildDiscriminatorSet)
                                .flatMap(Collection::stream))
                .collect(Collectors.toSet());
    }

    default HibernatePropertyIdentity getHibernateIdentity() {
        return Optional.ofNullable(getMappedForm())
                .map(Mapping::getIdentity)
                .or(this::resolveCompositeIdentity)
                .orElseGet(this::getDefaultIdentity);
    }

    private Optional<HibernatePropertyIdentity> resolveCompositeIdentity() {
        return Optional.ofNullable(getCompositeIdentity())
                .filter(compositeId -> compositeId.length > 1)
                .map(compositeId -> {
                    HibernateCompositeIdentity ci = new HibernateCompositeIdentity();
                    ci.setPropertyNames(java.util.Arrays.stream(compositeId)
                            .map(PersistentProperty::getName)
                            .toArray(String[]::new));
                    return ci;
                });
    }

    private @Nonnull HibernateSimpleIdentity getDefaultIdentity() {
        HibernateSimpleIdentity identity = new HibernateSimpleIdentity();
        identity.setName(Optional.ofNullable(getIdentity())
                .map(PersistentProperty::getName)
                .orElseGet(this::getName));
        return identity;
    }

    @Override
    HibernatePersistentProperty getIdentity();

    @Override
    HibernatePersistentProperty[] getCompositeIdentity();

    default Optional<HibernateCompositeIdentity> getHibernateCompositeIdentity() {
        return Optional.ofNullable(getMappedForm())
                .filter(Mapping::hasCompositeIdentifier)
                .map(Mapping::getIdentity)
                .filter(HibernateCompositeIdentity.class::isInstance)
                .map(HibernateCompositeIdentity.class::cast);
    }

    default String getDiscriminatorValue() {
        return Optional.ofNullable(getMappedForm())
                .map(Mapping::getDiscriminator)
                .map(DiscriminatorConfig::getValue)
                .orElse(getJavaClass().getSimpleName());
    }

    String getDataSourceName();

    void setDataSourceName(String dataSourceName);

    boolean forGrailsDomainMapping(String dataSourceName);

    boolean usesConnectionSource(String dataSourceName);

    boolean isAbstract();

    default List<HibernatePersistentProperty> getPersistentPropertiesToBind() {
        List<HibernatePersistentProperty> properties = getHibernatePersistentProperties();
        if (properties == null) {
            return java.util.Collections.emptyList();
        }
        return properties.stream()
                .filter(Objects::nonNull)
                .filter(p -> p.getMappedForm() != null)
                .filter(p -> !p.isIdentityProperty())
                .filter(p -> !GormProperties.VERSION.equals(p.getName()))
                .filter(p -> !p.isInherited())
                .toList();
    }

    @Override
    HibernatePersistentProperty getVersion();

    /**
     * Returns the persistent property with the given name cast to {@link HibernatePersistentProperty},
     * or {@code null} if no such property exists.
     */
    default HibernatePersistentProperty getHibernatePropertyByName(String name) {
        return (HibernatePersistentProperty) getPropertyByName(name);
    }

    /**
     * Returns the persistent property with the given path (e.g. "author.name") cast to {@link HibernatePersistentProperty},
     * or {@code null} if no such property exists.
     *
     * @param path The path to the property
     * @return The property or null
     */
    default HibernatePersistentProperty getHibernatePropertyByPath(String path) {
        if (path == null) return null;
        if (path.contains(".")) {
            String[] parts = path.split("\\.", 2);
            HibernatePersistentProperty prop = getHibernatePropertyByName(parts[0]);
            if (prop != null) {
                GrailsHibernatePersistentEntity associated = prop.getHibernateAssociatedEntity();
                if (associated != null) {
                    return associated.getHibernatePropertyByPath(parts[1]);
                }
            }
            return null;
        }
        return getHibernatePropertyByName(path);
    }

    /**
     * @param parentType The type of the parent entity
     * @return The parent property if it exists
     */
    default Optional<HibernatePersistentProperty> getHibernateParentProperty(Class<?> parentType) {
        List<HibernatePersistentProperty> properties = getHibernatePersistentProperties();
        if (properties == null) {
            return Optional.empty();
        }
        return properties.stream()
                .filter(Objects::nonNull)
                .filter(p -> p.getType().equals(parentType))
                .findFirst();
    }

    /**
     * @param parentType The type of the parent entity to exclude from the results
     * @return The properties that should be bound to the Hibernate meta model
     */
    default List<HibernatePersistentProperty> getHibernatePersistentProperties(Class<?> parentType) {
        List<HibernatePersistentProperty> properties = getHibernatePersistentProperties();
        if (properties == null) {
            return java.util.Collections.emptyList();
        }
        return properties.stream()
                .filter(Objects::nonNull)
                .filter(p -> p.getMappedForm() != null)
                .filter(p -> !p.equals(getIdentity()))
                .filter(p -> !GormProperties.VERSION.equals(p.getName()))
                .filter(p -> !p.getType().equals(parentType))
                .toList();
    }

    default List<HibernatePersistentEntity> getChildEntities() {
        return getChildEntities(getDataSourceName());
    }

    default List<HibernatePersistentEntity> getChildEntities(String dataSourceName) {
        return getMappingContext().getDirectChildEntities(this).stream()
                .filter(HibernatePersistentEntity.class::isInstance)
                .map(HibernatePersistentEntity.class::cast)
                .filter(persistentEntity -> persistentEntity.usesConnectionSource(dataSourceName))
                .filter(sub -> sub.getJavaClass().getSuperclass().equals(this.getJavaClass()))
                .toList();
    }

    default boolean isComponentPropertyNullable(PersistentProperty<?> embeddedProperty) {
        if (embeddedProperty == null) return false;
        final Mapping mapping = getMappedForm();
        return !isRoot() && (mapping == null || mapping.isTablePerHierarchy()) || embeddedProperty.isNullable();
    }

    default void configureDerivedProperties() {
        getHibernatePersistentProperties().forEach(new ConfigureDerivedPropertiesConsumer(getMappedForm()));
    }

    default HibernatePersistentProperty getHibernateTenantId() {
        return (HibernatePersistentProperty) getTenantId();
    }

    default String getMultiTenantFilterCondition(DefaultColumnNameFetcher fetcher) {
        return Optional.ofNullable(getHibernateTenantId())
                .map(fetcher::getDefaultColumnName)
                .map(defaultColumnName -> ":tenantId = " + defaultColumnName)
                .orElse(null);
    }

    default String getSchema(@Nonnull InFlightMetadataCollector mappings) {
        return Optional.ofNullable(getMappedForm())
                .map(Mapping::getTable)
                .map(org.grails.orm.hibernate.cfg.Table::getSchema)
                .orElse(NamespaceNameExtractor.getSchemaName(mappings));
    }

    default String getCatalog(@Nonnull InFlightMetadataCollector mappings) {
        return Optional.ofNullable(getMappedForm())
                .map(Mapping::getTable)
                .map(org.grails.orm.hibernate.cfg.Table::getCatalog)
                .orElse(NamespaceNameExtractor.getCatalogName(mappings));
    }

    /**
     * Evaluates the table name for the given entity
     *
     * @param persistentEntityNamingStrategy The naming strategy
     * @return The table name
     */
    default String getTableName(PersistentEntityNamingStrategy persistentEntityNamingStrategy) {
        return Optional.ofNullable(getMappedForm())
                .map(Mapping::getTableName)
                .or(() -> Optional.ofNullable(getRootMapping())
                        .filter(Mapping::isTablePerHierarchy)
                        .map(Mapping::getTableName))
                .orElseGet(() -> persistentEntityNamingStrategy.resolveTableName(this));
    }

    default String getDiscriminatorColumnName() {
        return Optional.ofNullable(getRootMapping())
                .map(Mapping::getDiscriminator)
                .map(GrailsHibernatePersistentEntity::resolveDiscriminatorValue)
                .orElse(JPA_DEFAULT_DISCRIMINATOR_TYPE);
    }

    default List<HibernatePersistentProperty> getHibernatePersistentProperties() {
        return getPersistentProperties().stream()
                .filter(HibernatePersistentProperty.class::isInstance)
                .map(HibernatePersistentProperty.class::cast)
                .map(HibernatePersistentProperty::validateProperty)
                .toList();
    }

    default String getComment() {
        return Optional.ofNullable(getMappedForm()).map(Mapping::getComment).orElse(null);
    }

    default Mapping getHibernateMappedForm() {
        return getMappedForm();
    }

    PersistentClass getPersistentClass();

    void setPersistentClass(PersistentClass persistentClass);

    /**
     * Determines if the given property should be lazy.
     *
     * @param property The property
     * @return True if it should be lazy
     */
    default boolean isLazy(HibernatePersistentProperty property) {
        if (GormProperties.VERSION.equals(property.getName())) {
            return false;
        }

        return Optional.ofNullable(property.getMappedForm())
                .map(config -> {
                    if (property instanceof HibernateAssociation && FetchMode.JOIN.equals(config.getFetchMode())) {
                        return false;
                    }
                    return config.getLazy();
                })
                .orElseGet(() -> property instanceof HibernateAssociation);
    }

    /**
     * Sorts or indexes the columns of {@code value} to align with this entity's composite
     * identifier order. When the identifier is a {@link Component} with an established sort order,
     * delegates to {@link SimpleValue#sortColumns(int[])}. Otherwise assigns sequential
     * {@link Column#setTypeIndex(int)} values so Hibernate can correlate them.
     *
     * @param value the foreign-key {@link SimpleValue} whose columns should be aligned
     */
    default void sortOrIndexForeignKeyColumns(SimpleValue value) {
        PersistentClass pc = getPersistentClass();
        KeyValue identifier = pc != null ? pc.getIdentifier() : null;
        int[] originalOrder = identifier instanceof Component c ? c.sortProperties() : null;
        if (originalOrder != null) {
            value.sortColumns(originalOrder);
        } else {
            List<Column> cols = value.getColumns();
            for (int i = 0; i < cols.size(); i++) {
                cols.get(i).setTypeIndex(i);
            }
        }
    }

    /**
     * Returns the identifier columns for the given {@code propertyNames} in the order that aligns
     * with the sorted foreign-key layout produced by {@link #sortOrIndexForeignKeyColumns}.
     * <p>
     * When the identifier is a {@link Component}, columns are gathered per property name and then
     * reordered according to the same permutation used during {@link Component#sortProperties()}.
     * When the identifier is a plain {@link KeyValue}, its columns are returned directly.
     *
     * @param propertyNames composite identity property names in the caller's declared order
     * @return identifier columns aligned with the foreign-key column layout, or an empty list if
     *         this entity has no persistent class or identifier
     */
    default List<Column> getReferencedIdentifierColumns(String[] propertyNames) {
        PersistentClass pc = getPersistentClass();
        KeyValue identifier = pc != null ? pc.getIdentifier() : null;
        if (identifier == null) {
            return List.of();
        }
        if (!(identifier instanceof Component component)) {
            return identifier.getColumns();
        }
        int[] originalOrder = component.sortProperties();
        List<Column> referencedColumns = Arrays.stream(propertyNames)
                .flatMap(name -> component.getProperty(name).getValue().getColumns().stream())
                .collect(Collectors.toCollection(ArrayList::new));
        return originalOrder != null ? sortedByPermutation(referencedColumns, originalOrder) : referencedColumns;
    }

    private static List<Column> sortedByPermutation(List<Column> columns, int[] permutation) {
        List<Column> result = new ArrayList<>(columns);
        for (int i = 0; i < permutation.length; i++) {
            result.set(permutation[i], columns.get(i));
        }
        return result;
    }
}
