/*
 * 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.quarkus.test.support.jdbc;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import io.agroal.api.AgroalDataSource;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.apache.camel.CamelContext;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.quarkus.test.support.jdbc.model.Camel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RegisterForReflection
public class BaseCamelJdbcResource {
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseCamelJdbcResource.class);
    ProducerTemplate template;

    CamelContext context;
    String dbKind;

    public void initialize(AgroalDataSource dataSource, String dbKind, CamelContext context, ProducerTemplate template)
            throws Exception {
        Connection conn = dataSource.getConnection();
        runScripts(conn, "droptables.sql");
        runScripts(conn, dbKind + ".sql");
        runScripts(conn, "inserts.sql");

        context.getRouteController().startRoute("jdbc-poll-" + dbKind);
        this.template = template;
        this.context = context;
        this.dbKind = dbKind;
    }

    @Path("/species/{id}")
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getSpeciesById(@PathParam("id") String id) throws Exception {
        return template.requestBody("jdbc:" + dbKind, "select species from camels where id = " + id, String.class);
    }

    @SuppressWarnings("unchecked")
    @Path("/species/{id}/list")
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getSpeciesByIdWithSelectList(@PathParam("id") String id) throws Exception {
        List<LinkedHashMap<String, Object>> result = template
                .requestBody("jdbc:" + dbKind + "?outputType=SelectList", "select * from camels where id = " + id, List.class);

        if (result.isEmpty()) {
            throw new IllegalStateException("Expected at least 1 camel result but none were found");
        }

        LinkedHashMap<String, Object> data = result.get(0);
        return data.get(getSpeciesRowName()) + " " + data.get(getIdRowName());
    }

    @SuppressWarnings("unchecked")
    @Path("/species/{id}/type")
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getSpeciesByIdWithDefinedType(@PathParam("id") String id) throws Exception {
        List<Camel> results = template.requestBody("jdbc:" + dbKind + "?outputClass=" + Camel.class.getName(),
                "select * from camels where id = " + id, List.class);

        if (results.isEmpty()) {
            throw new IllegalStateException("Expected at least 1 camel result but none were found");
        }

        Camel camel = results.get(0);
        return camel.getSpecies() + " " + camel.getId();
    }

    @Path("/execute")
    @POST
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.TEXT_PLAIN)
    public String executeStatement(String statement) throws Exception {
        return template.requestBody("jdbc:" + dbKind, statement, String.class);
    }

    @Path("/generated-keys/rows")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List generatedKeysRows() throws Exception {
        return template.requestBodyAndHeader("direct://get-generated-keys-" + dbKind,
                "insert into camelsGenerated (species) values ('Camelus testus')",
                "CamelRetrieveGeneratedKeys", "true", ArrayList.class);
    }

    @Path("/headers/insert")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String headersFromInsertOrUpdate() throws Exception {
        return template.requestBodyAndHeader("direct://get-headers-" + dbKind,
                "insert into camelsGenerated (species) values ('Camelus testus')",
                "CamelRetrieveGeneratedKeys", "true", String.class);
    }

    @Path("/headers/select")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String headersFromSelect() throws Exception {
        return template.requestBody("direct://get-headers-" + dbKind, "select * from camelsGenerated", String.class);
    }

    @Path("/named-parameters/headers-as-parameters")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String headersAsParameters() throws Exception {
        int id = 3;
        return template.requestBodyAndHeader("direct://headers-as-parameters-" + dbKind,
                "select * from camels where id < :?idmax order by id",
                "idmax", id, String.class);
    }

    @Path("/named-parameters/headers-as-parameters-map")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String headersAsParametersMap() throws Exception {
        Map<String, Object> headersMap = Map.of("idmax", 3, "specs", "Camelus bactrianus");
        return template.requestBodyAndHeader("direct://headers-as-parameters-" + dbKind,
                "select * from camels where id < :?idmax and species = :?specs order by id",
                "CamelJdbcParameters", headersMap, String.class);
    }

    @Path("/interval-polling")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public void intervalPolling(String selectResult) throws Exception {
        MockEndpoint mockEndpoint = context.getEndpoint("mock:interval-polling-" + dbKind, MockEndpoint.class);
        mockEndpoint.expectedBodiesReceived(selectResult);

        mockEndpoint.assertIsSatisfied();
    }

    @Path("/move-between-datasources")
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public String moveBetweenDatasources() throws Exception {
        return template.requestBody("direct://move-between-datasources-" + dbKind, null, String.class);
    }

    private void runScripts(Connection conn, String fileName) throws SQLException, IOException {
        try (Statement statement = conn.createStatement()) {
            try (InputStream is = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(fileName);
                    InputStreamReader isr = new InputStreamReader(is);
                    BufferedReader reader = new BufferedReader(isr)) {

                //execute each line from the sql script as separate statement
                reader.lines().filter(s -> s != null && !"".equals(s) && !s.startsWith("--")).forEach(s -> {
                    try {
                        statement.execute(s);
                    } catch (SQLException e) {
                        if (!s.toUpperCase().startsWith("DROP")) {
                            throw new RuntimeException(e);
                        } else {
                            LOGGER.debug(String.format("Command '%s' failed.", s)); //use debug logging
                        }
                    }
                });
            }
        }
    }

    private String getIdRowName() {
        if (dbKind.equals("h2") || dbKind.equals("oracle") || dbKind.equals("db2")) {
            return "ID";
        } else {
            return "id";
        }
    }

    private String getSpeciesRowName() {
        if (dbKind.equals("h2") || dbKind.equals("oracle") || dbKind.equals("db2")) {
            return "SPECIES";
        } else {
            return "species";
        }
    }
}
