/*
 * 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.tinkerpop.gremlin.groovy.jsr223.dsl.credential;

import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.mindrot.jbcrypt.BCrypt;

import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.drop;

/**
 * A DSL for managing a "credentials graph" used by Gremlin Server for simple authentication functions.  If the
 * {@link Graph} is transactional, new transactions will be started for each method call.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public class CredentialGraph {

    private final int BCRYPT_ROUNDS = 4;
    private final Graph graph;
    private final GraphTraversalSource g;
    private final boolean supportsTransactions;

    public CredentialGraph(final Graph graph) {
        this.graph = graph;
        g = graph.traversal();
        supportsTransactions = graph.features().graph().supportsTransactions();
    }

    /**
     * Finds a user by username and return {@code null} if one could not be found.
     *
     * @throws IllegalStateException if there is more than one user with a particular username.
     */
    public Vertex findUser(final String username) {
        if (supportsTransactions) g.tx().rollback();
        final GraphTraversal<Vertex,Vertex> t = g.V().has(CredentialGraphTokens.PROPERTY_USERNAME, username);
        final Vertex v = t.hasNext() ? t.next() : null;
        if (t.hasNext()) throw new IllegalStateException(String.format("Multiple users with username %s", username));
        return v;
    }

    /**
     * Creates a new user.
     *
     * @return the newly created user vertex
     */
    public Vertex createUser(final String username, final String password) {
        if (findUser(username) != null) throw new IllegalStateException("User with this name already exists");
        if (supportsTransactions) graph.tx().rollback();

        try {
            final Vertex v =  graph.addVertex(T.label, CredentialGraphTokens.VERTEX_LABEL_USER,
                                              CredentialGraphTokens.PROPERTY_USERNAME, username,
                                              CredentialGraphTokens.PROPERTY_PASSWORD, BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_ROUNDS)));
            if (supportsTransactions) graph.tx().commit();
            return v;
        } catch (Exception ex) {
            if (supportsTransactions) graph.tx().rollback();
            throw new RuntimeException(ex);
        }
    }

    /**
     * Removes a user by name.
     *
     * @return the number of users removed (which should be one or zero)
     */
    public long removeUser(final String username) {
        if (supportsTransactions) graph.tx().rollback();
        try {
            final long count = g.V().has(CredentialGraphTokens.PROPERTY_USERNAME, username).sideEffect(drop()).count().next();
            if (supportsTransactions) graph.tx().commit();
            return count;
        } catch (Exception ex) {
            if (supportsTransactions) graph.tx().rollback();
            throw new RuntimeException(ex);
        }
    }

    /**
     * Get a count of the number of users in the database.
     */
    public long countUsers() {
        if (supportsTransactions) graph.tx().rollback();
        return g.V().hasLabel(CredentialGraphTokens.VERTEX_LABEL_USER).count().next();
    }

    @Override
    public String toString() {
        return "CredentialGraph{" +
                "graph=" + graph +
                '}';
    }

    /**
     * Wrap up any {@link Graph} instance in the {@code CredentialGraph} DSL.
     */
    public static CredentialGraph credentials(final Graph graph) {
        return new CredentialGraph(graph);
    }
}
