/*
 * 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.component.couchdb;

import java.net.URI;
import java.util.Map;

import com.ibm.cloud.cloudant.v1.Cloudant;
import com.ibm.cloud.sdk.core.security.Authenticator;
import com.ibm.cloud.sdk.core.security.BasicAuthenticator;
import com.ibm.cloud.sdk.core.security.NoAuthAuthenticator;
import org.apache.camel.Category;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.spi.*;
import org.apache.camel.support.DefaultEndpoint;

/**
 * Consume changesets for inserts, updates and deletes in a CouchDB database, as well as get, save, update and delete
 * documents from a CouchDB database.
 */
@UriEndpoint(firstVersion = "2.11.0", scheme = "couchdb", title = "CouchDB", syntax = "couchdb:protocol:hostname:port/database",
             category = { Category.DATABASE }, headersClass = CouchDbConstants.class)
public class CouchDbEndpoint extends DefaultEndpoint implements EndpointServiceLocation {

    public static final String DEFAULT_STYLE = "main_only";
    public static final long DEFAULT_HEARTBEAT = 30000;
    public static final int DEFAULT_PORT = 5984;

    private static final String URI_ERROR
            = "Invalid URI. Format must be of the form couchdb:http[s]://hostname[:port]/database?[options...]";

    @UriPath(enums = "http,https")
    @Metadata(required = true)
    private String protocol;
    @UriPath
    @Metadata(required = true)
    private String hostname;
    @UriPath(defaultValue = "" + DEFAULT_PORT)
    private int port;
    @UriPath
    @Metadata(required = true)
    private String database;
    @UriParam(label = "consumer", enums = "all_docs,main_only", defaultValue = DEFAULT_STYLE)
    private String style = DEFAULT_STYLE;
    @UriParam(label = "security", secret = true)
    private String username;
    @UriParam(label = "security", secret = true)
    private String password;
    @UriParam(label = "consumer", defaultValue = "" + DEFAULT_HEARTBEAT, javaType = "java.time.Duration")
    private long heartbeat = DEFAULT_HEARTBEAT;
    @UriParam
    private boolean createDatabase;
    @UriParam(label = "consumer", defaultValue = "true")
    private boolean deletes = true;
    @UriParam(label = "consumer", defaultValue = "true")
    private boolean updates = true;
    @UriParam(label = "consumer", defaultValue = "10")
    private int maxMessagesPerPoll = 10;

    public CouchDbEndpoint() {
    }

    public CouchDbEndpoint(String endpointUri, String remaining, CouchDbComponent component) throws Exception {
        super(endpointUri, component);

        URI uri = new URI(remaining);

        protocol = uri.getScheme();
        if (protocol == null) {
            throw new IllegalArgumentException(URI_ERROR);
        }

        port = uri.getPort() == -1 ? DEFAULT_PORT : uri.getPort();

        if (uri.getPath() == null || uri.getPath().isBlank()) {
            throw new IllegalArgumentException(URI_ERROR);
        }
        database = uri.getPath().substring(1);

        hostname = uri.getHost();
        if (hostname == null) {
            throw new IllegalArgumentException(URI_ERROR);
        }
    }

    @Override
    public String getServiceUrl() {
        return getProtocol() + ":" + getHostname() + ":" + getPort();
    }

    @Override
    public String getServiceProtocol() {
        return getProtocol();
    }

    @Override
    public Map<String, String> getServiceMetadata() {
        if (username != null) {
            return Map.of("username", username);
        }
        return null;
    }

    @Override
    public Consumer createConsumer(Processor processor) throws Exception {
        CouchDbConsumer answer = new CouchDbConsumer(this, createClient(), processor);
        answer.setMaxMessagesPerPoll(getMaxMessagesPerPoll());
        configureConsumer(answer);
        return answer;
    }

    @Override
    public Producer createProducer() throws Exception {
        return new CouchDbProducer(this, createClient());
    }

    protected CouchDbClientWrapper createClient() {
        Authenticator authenticator;
        if (username == null) {
            authenticator = new NoAuthAuthenticator();
        } else {
            authenticator = new BasicAuthenticator(username, password);
        }

        Cloudant cloudant = new Cloudant("camel-couchdb", authenticator);
        cloudant.setServiceUrl(getServiceUrl());

        return new CouchDbClientWrapper(cloudant, database, createDatabase);
    }

    public String getProtocol() {
        return protocol;
    }

    /**
     * The protocol to use for communicating with the database.
     */
    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public String getHostname() {
        return hostname;
    }

    /**
     * Hostname of the running couchdb instance
     */
    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    public String getStyle() {
        return style;
    }

    /**
     * Specifies how many revisions are returned in the changes array. The default, main_only, will only return the
     * current "winning" revision; all_docs will return all leaf revisions (including conflicts and deleted former
     * conflicts.)
     */
    public void setStyle(String style) {
        this.style = style;
    }

    public String getUsername() {
        return username;
    }

    /**
     * Username in case of authenticated databases
     */
    public void setUsername(String username) {
        this.username = username;
    }

    public String getDatabase() {
        return database;
    }

    /**
     * Name of the database to use
     */
    public void setDatabase(String database) {
        this.database = database;
    }

    public String getPassword() {
        return password;
    }

    /**
     * Password for authenticated databases
     */
    public void setPassword(String password) {
        this.password = password;
    }

    public int getPort() {
        return port;
    }

    /**
     * Port number for the running couchdb instance
     */
    public void setPort(int port) {
        this.port = port;
    }

    public long getHeartbeat() {
        return heartbeat;
    }

    /**
     * How often to send an empty message to keep socket alive in millis
     */
    public void setHeartbeat(long heartbeat) {
        this.heartbeat = heartbeat;
    }

    public boolean isCreateDatabase() {
        return createDatabase;
    }

    /**
     * Creates the database if it does not already exist
     */
    public void setCreateDatabase(boolean createDatabase) {
        this.createDatabase = createDatabase;
    }

    public boolean isDeletes() {
        return deletes;
    }

    /**
     * Document deletes are published as events
     */
    public void setDeletes(boolean deletes) {
        this.deletes = deletes;
    }

    public boolean isUpdates() {
        return updates;
    }

    /**
     * Document inserts/updates are published as events
     */
    public void setUpdates(boolean updates) {
        this.updates = updates;
    }

    public int getMaxMessagesPerPoll() {
        return maxMessagesPerPoll;
    }

    /**
     * Gets the maximum number of messages as a limit to poll at each polling.
     * <p/>
     * Gets the maximum number of messages as a limit to poll at each polling. The default value is 10. Use 0 or a
     * negative number to set it as unlimited.
     */
    public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
        this.maxMessagesPerPoll = maxMessagesPerPoll;
    }
}
