/*
 * 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.camel.springboot.openapi;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import io.apicurio.datamodels.openapi.models.OasDocument;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.camel.CamelContext;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.openapi.BeanConfig;
import org.apache.camel.openapi.DefaultRestDefinitionsResolver;
import org.apache.camel.openapi.RestDefinitionsResolver;
import org.apache.camel.openapi.RestOpenApiReader;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spring.boot.CamelContextConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;

import java.util.List;
import java.util.Map;

import static org.apache.camel.openapi.OpenApiHelper.clearVendorExtensions;

/**
 * Open API auto-configuration.
 */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({OpenApiConfiguration.class})
@ConditionalOnBean(type = "org.apache.camel.spring.boot.CamelAutoConfiguration")
@ConditionalOnProperty(name = "camel.openapi.enabled", matchIfMissing = true)
@AutoConfigureAfter(name = "org.apache.camel.spring.boot.CamelAutoConfiguration")
@AutoConfigureBefore(name = "org.springdoc.core.SpringDocConfiguration")
public class OpenApiAutoConfiguration {

    private static final Logger LOG = LoggerFactory.getLogger(OpenApiAutoConfiguration.class);

    private final RestOpenApiReader reader = new RestOpenApiReader();
    private final RestDefinitionsResolver resolver = new DefaultRestDefinitionsResolver();
    private final OpenAPI openAPI = new OpenAPI();

    @Bean
    CamelContextConfiguration onBeforeStart(GenericApplicationContext ac, CamelContext camelContext, OpenApiConfiguration config) {
        return new CamelContextConfiguration() {
            @Override
            public void beforeApplicationStart(CamelContext camelContext) {
                // routes have now been loaded, so we need to detect rest-dsl APIs in Camel
                // this will trigger spring boot to create the bean which springdoc can detect
                try {
                    OpenAPI created = createOpenAPI(camelContext);
                    if (created != null) {
                        LOG.info("OpenAPI ({}) created from Camel Rest-DSL v{} - {}", created.getOpenapi(), created.getInfo().getVersion(), created.getInfo().getTitle());
                        // transfer data to the existing
                        openAPI.setInfo(created.getInfo());
                        openAPI.setOpenapi(created.getOpenapi());
                        if (created.getComponents() != null) {
                            openAPI.setComponents(created.getComponents());
                        }
                        if (created.getExtensions() != null) {
                            openAPI.setExtensions(created.getExtensions());
                        }
                        if (created.getSecurity() != null) {
                            openAPI.setSecurity(created.getSecurity());
                        }
                        if (created.getExternalDocs() != null) {
                            openAPI.setExternalDocs(created.getExternalDocs());
                        }
                        if (created.getPaths() != null) {
                            openAPI.setPaths(created.getPaths());
                        }
                        if (created.getTags() != null) {
                            openAPI.setTags(created.getTags());
                        }
                        // do not copy servers as we use the spring-boot configured setting
                    }
                } catch (Exception e) {
                    LOG.warn("Error generating OpenAPI from Camel Rest DSL due to: " + e.getMessage() + ". This exception is ignored.", e);
                }
            }

            @Override
            public void afterApplicationStart(CamelContext camelContext) {
                // noop
            }
        };
    }

    @Bean
    OpenAPI camelRestDSLOpenApi() {
        // due to ordering how beans are resolved and setup in spring boot, then we need to create
        // this provisional bean which is later updated with the actual Rest DSL APIs after the routes has been loaded
        // into Camel
        return openAPI;
    }

    private OpenAPI createOpenAPI(CamelContext camelContext) throws Exception {
        List<RestDefinition> rests = resolver.getRestDefinitions(camelContext, null);
        if (rests == null || rests.isEmpty()) {
            return null;
        }

        BeanConfig bc = new BeanConfig();
        Info info = new Info();

        RestConfiguration rc = camelContext.getRestConfiguration();
        initOpenApi(bc, info, rc.getApiProperties());

        OasDocument openApi = reader.read(camelContext, rests, bc, null, camelContext.getClassResolver());
        if (!rc.isApiVendorExtension()) {
            clearVendorExtensions(openApi);
        }

        // dump to json
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        Object dump = io.apicurio.datamodels.Library.writeNode(openApi);
        byte[] jsonData = mapper.writeValueAsBytes(dump);
        // json to yaml
        JsonNode node = mapper.readTree(jsonData);
        String yaml = new YAMLMapper().writeValueAsString(node);

        // parse bytes into swagger
        OpenAPIV3Parser parser = new OpenAPIV3Parser();
        SwaggerParseResult spr = parser.readContents(yaml);
        OpenAPI answer = spr.getOpenAPI();
        if (answer != null) {
            answer.setInfo(info);
        }
        return answer;
    }

    private static void initOpenApi(BeanConfig bc, Info info, Map<String, Object> config) {
        // configure openApi options
        String s = (String) config.get("openapi.version");
        if (s != null) {
            bc.setVersion(s);
        }
        s = (String) config.get("base.path");
        if (s != null) {
            bc.setBasePath(s);
        }
        s = (String) config.get("host");
        if (s != null) {
            bc.setHost(s);
        }
        s = (String) config.get("schemes");
        if (s == null) {
            // deprecated due typo
            s = (String) config.get("schemas");
        }
        if (s != null) {
            String[] schemes = s.split(",");
            bc.setSchemes(schemes);
        } else {
            // assume http by default
            bc.setSchemes(new String[] { "http" });
        }

        String version = (String) config.get("api.version");
        String title = (String) config.get("api.title");
        String description = (String) config.get("api.description");
        String termsOfService = (String) config.get("api.termsOfService");
        String licenseName = (String) config.get("api.license.name");
        String licenseUrl = (String) config.get("api.license.url");
        String contactName = (String) config.get("api.contact.name");
        String contactUrl = (String) config.get("api.contact.url");
        String contactEmail = (String) config.get("api.contact.email");

        bc.setTitle(title);
        bc.setLicense(licenseName);
        bc.setLicenseUrl(licenseUrl);

        info.setTitle(title);
        info.setVersion(version);
        info.setDescription(description);
        info.setTermsOfService(termsOfService);
        if (contactName != null) {
            Contact contact = new Contact();
            contact.setName(contactName);
            contact.setEmail(contactEmail);
            contact.setUrl(contactUrl);
            info.setContact(contact);
        }
    }

}
