/*
 *  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.viewer.graphql.model.types;

import static graphql.schema.GraphQLNonNull.nonNull;
import static graphql.schema.GraphQLTypeReference.typeRef;

import jakarta.inject.Inject;
import jakarta.inject.Provider;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.jspecify.annotations.Nullable;

import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
import org.apache.causeway.core.metamodel.spec.feature.OneToManyActionParameter;
import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.causeway.core.metamodel.spec.feature.OneToOneFeature;
import org.apache.causeway.viewer.graphql.model.context.Context;
import org.apache.causeway.viewer.graphql.model.domain.SchemaType;
import org.apache.causeway.viewer.graphql.model.domain.TypeNames;

import graphql.Scalars;
import graphql.schema.GraphQLInputType;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLTypeReference;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(onConstructor_ = {@Inject})
public class TypeMapperDefault implements TypeMapper {

    @Configuration
    public static class AutoConfiguration {

        @Bean
        @ConditionalOnMissingBean(TypeMapper.class)
        public TypeMapper defaultTypeMapper(final ScalarMapper scalarMapper, final Provider<Context> contextProvider) {
            return new TypeMapperDefault(scalarMapper, contextProvider);
        }
    }

    private final ScalarMapper scalarMapper;
    private final Provider<Context> contextProvider;

    @Override
    public GraphQLOutputType outputTypeFor(final Class<?> clazz){
        if (clazz.isEnum()) {
            return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH);
        }
        return scalarMapper.scalarTypeFor(clazz);
    }

    @Override
    public GraphQLInputType inputTypeFor(final Class<?> clazz){
        if (clazz.isEnum()) {
            return contextProvider.get().graphQLTypeRegistry.addEnumTypeIfNotAlreadyPresent(clazz, SchemaType.RICH);
        }
        return scalarMapper.scalarTypeFor(clazz);
    }

    @Override
    public Object unmarshal(
            final Object gqlValue,
            final ObjectSpecification targetObjectSpec) {
        var correspondingClass = targetObjectSpec.getCorrespondingClass();
        if (correspondingClass.isEnum()) {
            return gqlValue;
        }
        return scalarMapper.unmarshal(gqlValue, correspondingClass);
    }

    @Override
    public GraphQLOutputType outputTypeFor(
            final OneToOneFeature oneToOneFeature,
            final SchemaType schemaType) {
        ObjectSpecification otoaObjectSpec = oneToOneFeature.getElementType();

        return switch (otoaObjectSpec.getBeanSort()) {
            case VIEW_MODEL, ENTITY -> typeRefPossiblyOptional(oneToOneFeature, schemaType, otoaObjectSpec);
            case VALUE-> scalarTypePossiblyOptional(oneToOneFeature, otoaObjectSpec);
            default -> null;
        };
    }

    private static GraphQLOutputType typeRefPossiblyOptional(OneToOneFeature oneToOneFeature, SchemaType schemaType, ObjectSpecification otoaObjectSpec) {
        GraphQLTypeReference fieldTypeRef = typeRef(TypeNames.objectTypeNameFor(otoaObjectSpec, schemaType));
        return oneToOneFeature.isOptional()
                ? fieldTypeRef
                : nonNull(fieldTypeRef);
    }

    private GraphQLOutputType scalarTypePossiblyOptional(OneToOneFeature oneToOneFeature, ObjectSpecification otoaObjectSpec) {
        GraphQLOutputType scalarType = outputTypeFor(otoaObjectSpec.getCorrespondingClass());
        return oneToOneFeature.isOptional()
                ? scalarType
                : nonNull(scalarType);
    }

    @Override
    @Nullable
    public GraphQLOutputType outputTypeFor(
            final ObjectSpecification objectSpecification,
            final SchemaType schemaType){

        return switch (objectSpecification.getBeanSort()){
            case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.objectTypeNameFor(objectSpecification, schemaType));
            case VALUE -> outputTypeFor(objectSpecification.getCorrespondingClass());
            case COLLECTION -> null; // should be noop
            default -> Scalars.GraphQLString; // for now
        };
    }

    @Override
    @Nullable public GraphQLList listTypeForElementTypeOf(
            final OneToManyAssociation oneToManyAssociation,
            final SchemaType schemaType) {
        var elementType = oneToManyAssociation.getElementType();
        return listTypeFor(elementType, schemaType);
    }

    @Override
    @Nullable public GraphQLList listTypeFor(
            final ObjectSpecification elementType,
            final SchemaType schemaType) {
        return switch (elementType.getBeanSort()) {
            case VIEW_MODEL, ENTITY ->
                GraphQLList.list(typeRef(TypeNames.objectTypeNameFor(elementType, schemaType)));
            case VALUE -> 
                GraphQLList.list(outputTypeFor(elementType.getCorrespondingClass()));
            default -> null;    
        };
    }

    @Override
    public GraphQLInputType inputTypeFor(
            final OneToOneFeature oneToOneFeature,
            final InputContext inputContext,
            final SchemaType schemaType) {
        
        return oneToOneFeature.isOptional() || inputContext.isOptionalAlwaysAllowed()
                ? inputTypeFor_(oneToOneFeature, schemaType)
                : nonNull(inputTypeFor_(oneToOneFeature, schemaType));
    }

    private GraphQLInputType inputTypeFor_(
            final OneToOneFeature oneToOneFeature,
            final SchemaType schemaType){
        var elementObjectSpec = oneToOneFeature.getElementType();
        
        {   // guard introduced to intercept interfaces, which otherwise seem to break schema creation
            // due to missing type reference for given name
            var elementClass = elementObjectSpec.getCorrespondingClass();
            if(elementClass.isInterface()) return inputTypeFor(elementClass);
        }
        
        return switch (elementObjectSpec.getBeanSort()) {
            case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.inputTypeNameFor(elementObjectSpec, schemaType));
            case VALUE -> inputTypeFor(elementObjectSpec.getCorrespondingClass());
            case COLLECTION ->
                throw new IllegalArgumentException(String.format("OneToOneFeature '%s' is not expected to have a beanSort of COLLECTION", oneToOneFeature.getFeatureIdentifier().toString()));
            default -> Scalars.GraphQLString; // for now
        };
    }

    @Override
    public GraphQLList inputTypeFor(
            final OneToManyActionParameter oneToManyActionParameter,
            final SchemaType schemaType){
        var elementObjectSpec = oneToManyActionParameter.getElementType();
        return GraphQLList.list(inputTypeFor(elementObjectSpec, schemaType));
    }

    @Override
    public GraphQLInputType inputTypeFor(
            final ObjectSpecification elementType,
            final SchemaType schemaType){
        return switch (elementType.getBeanSort()) {
            case ABSTRACT, VIEW_MODEL, ENTITY -> typeRef(TypeNames.inputTypeNameFor(elementType, schemaType));
            case VALUE -> inputTypeFor(elementType.getCorrespondingClass());
            case COLLECTION -> 
                throw new IllegalArgumentException(String.format("ObjectSpec '%s' is not expected to have a beanSort of COLLECTION", elementType.getFullIdentifier()));
            default -> Scalars.GraphQLString; // for now
        };
    }

}
