package com.iqser.red.service.peristence.v1.server;

import java.util.Map;
import java.util.Optional;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.http.server.observation.ServerRequestObservationConvention;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.iqser.red.persistence.service.v1.external.api.impl.PersistenceServiceExternalApiConfiguration;
import com.iqser.red.persistence.service.v2.external.api.impl.PersistenceServiceExternalApiConfigurationV2;
import com.iqser.red.service.dictionarymerge.commons.DictionaryMergeService;
import com.iqser.red.service.persistence.management.v1.processor.PersistenceServiceProcessorConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.cache.PersistenceServiceExternalApiCacheConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.lifecycle.LifecycleProperties;
import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings;
import com.iqser.red.service.persistence.v1.internal.api.PersistenceServiceInternalApiConfiguration;
import com.iqser.red.storage.commons.StorageAutoConfiguration;
import com.knecon.fforesight.databasetenantcommons.DatabaseTenantCommonsAutoConfiguration;
import com.knecon.fforesight.jobscommons.JobsAutoConfiguration;
import com.knecon.fforesight.keycloakcommons.DefaultKeyCloakCommonsAutoConfiguration;
import com.knecon.fforesight.mongo.database.commons.MongoDatabaseCommonsAutoConfiguration;
import com.knecon.fforesight.swaggercommons.SpringDocAutoConfiguration;
import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration;
import com.knecon.fforesight.tenantcommons.MultiTenancyMessagingConfiguration;
import com.knecon.fforesight.tenantcommons.MultiTenancyWebConfiguration;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.aop.ObservedAspect;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@EnableAsync
@EnableRetry
@EnableScheduling
@EnableCaching
@EnableConfigurationProperties({FileManagementServiceSettings.class, LifecycleProperties.class})
@EnableMongoRepositories(basePackages = "com.iqser.red.service.persistence")
@ImportAutoConfiguration({StorageAutoConfiguration.class, JobsAutoConfiguration.class, DatabaseTenantCommonsAutoConfiguration.class, MultiTenancyAutoConfiguration.class, SpringDocAutoConfiguration.class, DefaultKeyCloakCommonsAutoConfiguration.class, MongoDatabaseCommonsAutoConfiguration.class})
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, CassandraAutoConfiguration.class, DataSourceAutoConfiguration.class, LiquibaseAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@Import({PersistenceServiceExternalApiConfigurationV2.class, PersistenceServiceExternalApiConfiguration.class, PersistenceServiceInternalApiConfiguration.class, PersistenceServiceExternalApiCacheConfiguration.class, MultiTenancyWebConfiguration.class, PersistenceServiceProcessorConfiguration.class, MessagingConfiguration.class, MultiTenancyMessagingConfiguration.class})
public class Application implements ApplicationContextAware {

    /**
     * Entry point to the service application.
     *
     * @param args Any command line parameter given upon startup.
     */
    public static void main(String[] args) {

        SpringApplication.run(Application.class, args);
    }


    @Bean
    public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {

        return new ObservedAspect(observationRegistry);
    }


    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {

        return new TimedAspect(registry);
    }


    @Bean
    public DictionaryMergeService dictionaryMergeService() {

        return new DictionaryMergeService();
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        var beanFactory = applicationContext.getAutowireCapableBeanFactory();

        try {
            var observationRegistry = beanFactory.getBean(ObservationRegistry.class);

            beanFactory.initializeBean(observationRegistry, "observationRegistry");
        } catch (Exception ignored) {
        }
    }


    @Bean
    @ConditionalOnEnabledTracing
    public ServerRequestObservationConvention customTagsRequestObservationConvention(@Value("${kubernetes.namespace}") String namespace,
                                                                                     @Value("${project.version}") String version) {

        return new DefaultServerRequestObservationConvention() {
            @Override
            public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
                // Make sure that KeyValues entries are already sorted by name for better performance
                return super.getLowCardinalityKeyValues(context).and(KeyValue.of("kubernetes.namespace", namespace), KeyValue.of("service.version", version), tenantId(context));
            }


            @Override
            public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {

                return super.getHighCardinalityKeyValues(context)
                        .and(getValueFromPathVariableOrRequestParam(context, "dossierId"),
                             getValueFromPathVariableOrRequestParam(context, "dossierTemplateId"),
                             getValueFromPathVariableOrRequestParam(context, "fileId"))
                        .and(getValueFromRequestAttribute(context, "dossierId"),
                             getValueFromRequestAttribute(context, "dossierTemplateId"),
                             getValueFromRequestAttribute(context, "fileId"));
            }


            private KeyValue tenantId(ServerRequestObservationContext context) {

                return KeyValue.of("tenantId",
                                   Optional.ofNullable(context.getCarrier().getHeader("X-Tenant-Id"))
                                           .orElse(""));
            }


            private KeyValue getValueFromPathVariableOrRequestParam(ServerRequestObservationContext context, String name) {

                var value = context.getCarrier().getParameter(name);
                if (value == null) {
                    var pathVariables = (Map) context.getCarrier().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
                    value = pathVariables == null ? null : (String) pathVariables.get(name);
                }

                return KeyValue.of(name,
                                   Optional.ofNullable(value)
                                           .orElse(""));
            }


            private KeyValue getValueFromRequestAttribute(ServerRequestObservationContext context, String name) {

                Object value = context.getCarrier().getAttribute(name);
                return KeyValue.of(name, value == null ? "" : value.toString());
            }

        };
    }

}
