/*
 * 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.brooklyn.entity.nosql.mongodb;

import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.config.render.RendererHints;
import org.apache.brooklyn.core.location.access.BrooklynAccessUtils;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.feed.function.FunctionPollConfig;
import org.bson.BasicBSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Functions;
import com.google.common.base.Objects;
import com.google.common.net.HostAndPort;

public class MongoDBServerImpl extends SoftwareProcessImpl implements MongoDBServer {

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

    static {
        RendererHints.register(HTTP_INTERFACE_URL, RendererHints.namedActionWithUrl());
    }

    private FunctionFeed serviceStats;
    private FunctionFeed replicaSetStats;
    private MongoDBClientSupport client;

    public MongoDBServerImpl() {
    }

    @Override
    public Class<?> getDriverInterface() {
        return MongoDBDriver.class;
    }

    @Override
    protected void connectSensors() {
        super.connectSensors();
        connectServiceUpIsRunning();

        int port = sensors().get(MongoDBServer.PORT);
        HostAndPort accessibleAddress = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, port);
        sensors().set(MONGO_SERVER_ENDPOINT, String.format("%s:%d",
                accessibleAddress.getHostText(), accessibleAddress.getPort()));

        int httpConsolePort = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, sensors().get(HTTP_PORT)).getPort();
        sensors().set(HTTP_INTERFACE_URL, String.format("http://%s:%d",
                accessibleAddress.getHostText(), httpConsolePort));

        if (clientAccessEnabled()) {
            try {
                client = MongoDBClientSupport.forServer(this);
            } catch (UnknownHostException e) {
                LOG.warn("Unable to create client connection to {}, not connecting sensors: {} ", this, e.getMessage());
                return;
            }
    
            serviceStats = FunctionFeed.builder()
                    .entity(this)
                    .poll(new FunctionPollConfig<Object, BasicBSONObject>(STATUS_BSON)
                            .period(2, TimeUnit.SECONDS)
                            .callable(new Callable<BasicBSONObject>() {
                                @Override
                                public BasicBSONObject call() throws Exception {
                                    return MongoDBServerImpl.this.sensors().get(SERVICE_UP)
                                        ? client.getServerStatus()
                                        : null;
                                }
                            })
                            .onException(Functions.<BasicBSONObject>constant(null)))
                    .build();
    
            if (isReplicaSetMember()) {
                replicaSetStats = FunctionFeed.builder()
                        .entity(this)
                        .poll(new FunctionPollConfig<Object, ReplicaSetMemberStatus>(REPLICA_SET_MEMBER_STATUS)
                                .period(2, TimeUnit.SECONDS)
                                .callable(new Callable<ReplicaSetMemberStatus>() {
                                    /**
                                     * Calls {@link MongoDBClientSupport#getReplicaSetStatus} and
                                     * extracts <code>myState</code> from the response.
                                     * @return
                                     *      The appropriate {@link org.apache.brooklyn.entity.nosql.mongodb.ReplicaSetMemberStatus}
                                     *      if <code>myState</code> was non-null, {@link ReplicaSetMemberStatus#UNKNOWN} otherwise.
                                     */
                                    @Override
                                    public ReplicaSetMemberStatus call() {
                                        BasicBSONObject serverStatus = client.getReplicaSetStatus();
                                        int state = serverStatus.getInt("myState", -1);
                                        return ReplicaSetMemberStatus.fromCode(state);
                                    }
                                })
                                .onException(Functions.constant(ReplicaSetMemberStatus.UNKNOWN))
                                .suppressDuplicates(true))
                        .build();
            } else {
                sensors().set(IS_PRIMARY_FOR_REPLICA_SET, false);
                sensors().set(IS_SECONDARY_FOR_REPLICA_SET, false);
            }
        } else {
            LOG.info("Not monitoring "+this+" to retrieve state via client API");
        }

        // Take interesting details from STATUS.
        subscriptions().subscribe(this, STATUS_BSON, new SensorEventListener<BasicBSONObject>() {
                @Override public void onEvent(SensorEvent<BasicBSONObject> event) {
                    BasicBSONObject map = event.getValue();
                    if (map != null && !map.isEmpty()) {
                        sensors().set(UPTIME_SECONDS, map.getDouble("uptime", 0));

                        // Operations
                        BasicBSONObject opcounters = (BasicBSONObject) map.get("opcounters");
                        sensors().set(OPCOUNTERS_INSERTS, opcounters.getLong("insert", 0));
                        sensors().set(OPCOUNTERS_QUERIES, opcounters.getLong("query", 0));
                        sensors().set(OPCOUNTERS_UPDATES, opcounters.getLong("update", 0));
                        sensors().set(OPCOUNTERS_DELETES, opcounters.getLong("delete", 0));
                        sensors().set(OPCOUNTERS_GETMORE, opcounters.getLong("getmore", 0));
                        sensors().set(OPCOUNTERS_COMMAND, opcounters.getLong("command", 0));

                        // Network stats
                        BasicBSONObject network = (BasicBSONObject) map.get("network");
                        sensors().set(NETWORK_BYTES_IN, network.getLong("bytesIn", 0));
                        sensors().set(NETWORK_BYTES_OUT, network.getLong("bytesOut", 0));
                        sensors().set(NETWORK_NUM_REQUESTS, network.getLong("numRequests", 0));

                        // Replica set stats
                        BasicBSONObject repl = (BasicBSONObject) map.get("repl");
                        if (isReplicaSetMember() && repl != null) {
                            sensors().set(IS_PRIMARY_FOR_REPLICA_SET, repl.getBoolean("ismaster"));
                            sensors().set(IS_SECONDARY_FOR_REPLICA_SET, repl.getBoolean("secondary"));
                            sensors().set(REPLICA_SET_PRIMARY_ENDPOINT, repl.getString("primary"));
                        }
                    }
                }
        });
    }

    @Override
    protected void disconnectSensors() {
        super.disconnectSensors();
        disconnectServiceUpIsRunning();
        if (serviceStats != null) serviceStats.stop();
        if (replicaSetStats != null) replicaSetStats.stop();
    }

    protected boolean clientAccessEnabled() {
        return Boolean.TRUE.equals(config().get(MongoDBServer.USE_CLIENT_MONITORING));
    }
    
    @Override
    public MongoDBReplicaSet getReplicaSet() {
        return config().get(MongoDBServer.REPLICA_SET);
    }

    @Override
    public boolean isReplicaSetMember() {
        return getReplicaSet() != null;
    }

    @Override
    public boolean initializeReplicaSet(String replicaSetName, Integer id) {
        return client.initializeReplicaSet(replicaSetName, id);
    }

    @Override
    public boolean addMemberToReplicaSet(MongoDBServer secondary, Integer id) {
        // TODO The attributes IS_PRIMARY_FOR_REPLICA_SET and REPLICA_SET_MEMBER_STATUS can be out-of-sync.
        // The former is obtained by an enricher that listens to STATUS_BSON (set by client.getServerStatus()).
        // The latter is set by a different feed doing client.getReplicaSetStatus().getInt("myState").
        // The ReplicaSet uses REPLICA_SET_MEMBER_STATUS to determine which node to call.
        // 
        // Relying on caller to respect the `false` result, to retry.
        if (!sensors().get(IS_PRIMARY_FOR_REPLICA_SET)) {
            LOG.warn("Attempted to add {} to replica set at server that is not primary: {}", secondary, this);
            return false;
        }
        if (!clientAccessEnabled()) {
            throw new IllegalStateException("client-access disabled for "+this+"; cannot add to replica set member "+secondary+" -> "+id);
        }
        return client.addMemberToReplicaSet(secondary, id);
    }

    @Override
    public boolean removeMemberFromReplicaSet(MongoDBServer server) {
        if (!sensors().get(IS_PRIMARY_FOR_REPLICA_SET)) {
            LOG.warn("Attempted to remove {} from replica set at server that is not primary: {}", server, this);
            return false;
        }
        if (!clientAccessEnabled()) {
            throw new IllegalStateException("client-access disabled for "+this+"; cannot remove from replica set member "+server);
        }
        return client.removeMemberFromReplicaSet(server);
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                .add("id", getId())
                .add("hostname", sensors().get(HOSTNAME))
                .add("port", sensors().get(PORT))
                .toString();
    }
}
