/*
 * Copyright (C) 2020 Grakn Labs
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package grakn.core.test.rule;

import com.datastax.oss.driver.api.core.CqlSession;
import grakn.core.common.config.Config;
import grakn.core.common.config.ConfigKey;
import grakn.core.kb.server.Session;
import grakn.core.kb.server.keyspace.Keyspace;
import grakn.core.server.Server;
import grakn.core.server.ServerFactory;
import grakn.core.server.keyspace.KeyspaceImpl;
import grakn.core.server.keyspace.KeyspaceManager;
import grakn.core.server.rpc.KeyspaceService;
import grakn.core.server.rpc.OpenRequest;
import grakn.core.server.rpc.ServerOpenRequest;
import grakn.core.server.rpc.SessionService;
import grakn.core.server.session.HadoopGraphFactory;
import grakn.core.server.session.JanusGraphFactory;
import grakn.core.server.session.SessionFactory;
import grakn.core.server.util.LockManager;
import io.grpc.ServerBuilder;
import org.junit.rules.ExternalResource;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * This rule is a test server rule which starts Cassandra and Grakn Core Server on random, unused ports.
 * It allows multiple test server instances to run concurrently.
 * It enables all of the integration tests to run concurrently on the same machine.
 */
public class GraknTestServer extends ExternalResource {
    private static final Path DEFAULT_SERVER_CONFIG_PATH = Paths.get("server/conf/grakn.properties");

    // Test Storage instance
    private final GraknTestStorage graknTestStorage;

    // Grakn Core Server
    private final Path serverConfigPath;
    protected Config serverConfig;
    protected Server graknServer;
    private int grpcPort;
    private SessionFactory sessionFactory;
    private KeyspaceManager keyspaceManager;

    /**
     * Construct Grakn Server and Grakn Storage with default configurations
     */
    public GraknTestServer() {
        this.serverConfigPath = DEFAULT_SERVER_CONFIG_PATH;
        graknTestStorage = new GraknTestStorage();
    }

    /**
     * Construct Grakn Server and Grakn Storage with custom configurations
     */
    public GraknTestServer(Path serverConfigPath, Path cassandraConfigPath) {
        this.serverConfigPath = serverConfigPath;
        graknTestStorage = new GraknTestStorage(cassandraConfigPath);
    }

    @Override
    public void before() {
        try {
            // Start Cassandra
            graknTestStorage.before();

            // half of this might be good to split into cassandra rule separately
            grpcPort = findUnusedLocalPort();
            serverConfig = createTestConfig();

            // Start Grakn Core Server
            System.out.println("Starting Grakn Core Server...");
            graknServer = createServer();
            graknServer.start();
            System.out.println("Grakn Core Server started");
        } catch (IOException e) {
            throw new RuntimeException("Cannot start components", e);
        }
    }

    @Override
    public void after() {
        try {
            graknTestStorage.after();
            graknServer.close();
        } catch (Exception e) {
            throw new RuntimeException("Could not shut down ", e);
        }
    }

    // Getters

    public String grpcUri() {
        return serverConfig.getProperty(ConfigKey.SERVER_HOST_NAME) + ":" + serverConfig.getProperty(ConfigKey.GRPC_PORT);
    }

    public Session sessionWithNewKeyspace() {
        Keyspace randomKeyspace = randomKeyspaceName();
        return session(randomKeyspace);
    }

    public Keyspace randomKeyspaceName() {
        return new KeyspaceImpl("a" + UUID.randomUUID().toString().replaceAll("-", ""));
    }

    public Session session(Keyspace keyspace) {
        return sessionFactory.session(keyspace);
    }

    public Session session(String name) {
        return sessionFactory.session(new KeyspaceImpl(name));
    }

    public SessionFactory sessionFactory() {
        return sessionFactory;
    }

    public Config serverConfig() {
        return serverConfig;
    }

    public List<Keyspace> keyspaces() {
        return new ArrayList<>(keyspaceManager.keyspaces());
    }

    public void deleteKeyspace(String keyspace) {
        keyspaceManager.delete(new KeyspaceImpl(keyspace));
    }

    private synchronized static int findUnusedLocalPort() throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(0)) {
            return serverSocket.getLocalPort();
        }
    }

    // Grakn Core Server helpers

    private Config createTestConfig() throws FileNotFoundException {
        InputStream testConfig = new FileInputStream(serverConfigPath.toFile());

        Config config = Config.read(testConfig);
        //Override gRPC port with a random free port
        config.setConfigProperty(ConfigKey.STORAGE_HOSTNAME, "127.0.0.1");
        config.setConfigProperty(ConfigKey.GRPC_PORT, grpcPort);
        //Override Storage Port used by Janus to communicate with Cassandra Backend
        config.setConfigProperty(ConfigKey.STORAGE_PORT, graknTestStorage.nativeTransportPort());
//        Override ports used by HadoopGraph
        config.setConfigProperty(ConfigKey.HADOOP_STORAGE_PORT, graknTestStorage.nativeTransportPort());

        return config;
    }

    private Server createServer() {
        // distributed locks
        LockManager lockManager = new LockManager();
        JanusGraphFactory janusGraphFactory = new JanusGraphFactory(serverConfig);
        HadoopGraphFactory hadoopGraphFactory = new HadoopGraphFactory(serverConfig);

        Integer storagePort = serverConfig.getProperty(ConfigKey.STORAGE_PORT);
        String storageHostname = serverConfig.getProperty(ConfigKey.STORAGE_HOSTNAME);
        // CQL cluster used by KeyspaceManager to fetch all existing keyspaces
        CqlSession cqlSession = CqlSession.builder()
                .addContactPoint(new InetSocketAddress(storageHostname, storagePort))
                .withLocalDatacenter("datacenter1")
                .build();

        sessionFactory = new SessionFactory(lockManager, janusGraphFactory, hadoopGraphFactory, serverConfig);
        keyspaceManager = new KeyspaceManager(cqlSession, janusGraphFactory, sessionFactory);

        OpenRequest requestOpener = new ServerOpenRequest(sessionFactory);

        io.grpc.Server serverRPC = ServerBuilder.forPort(grpcPort)
                .addService(new SessionService(requestOpener))
                .addService(new KeyspaceService(keyspaceManager))
                .build();

        return ServerFactory.createServer(serverRPC);
    }

}