/**
 *    Copyright (C) 2015 Mesosphere, Inc.
 *
 * Licensed 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 io.mesosphere.mesos.frameworks.cassandra.framework;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import io.mesosphere.mesos.frameworks.cassandra.scheduler.*;
import io.mesosphere.mesos.frameworks.cassandra.scheduler.api.*;
import io.mesosphere.mesos.frameworks.cassandra.scheduler.health.HealthReportService;
import io.mesosphere.mesos.frameworks.cassandra.scheduler.util.Env;
import io.mesosphere.mesos.util.Clock;
import io.mesosphere.mesos.util.ProtoUtils;
import io.mesosphere.mesos.util.SystemClock;
import org.apache.mesos.MesosSchedulerDriver;
import org.apache.mesos.Protos.Credential;
import org.apache.mesos.Protos.FrameworkInfo;
import org.apache.mesos.Scheduler;
import org.apache.mesos.state.State;
import org.apache.mesos.state.ZooKeeperState;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.intellij.lang.annotations.Language;
import org.joda.time.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;

import java.net.URI;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Lists.newArrayList;
import static io.mesosphere.mesos.frameworks.cassandra.CassandraFrameworkProtos.ExternalDc;
import static io.mesosphere.mesos.util.ProtoUtils.frameworkId;

public final class Main {
    private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static final Supplier<String> DEFAULT_DATA_DIRECTORY = new Supplier<String>() {
        @Override
        public String get() {
            LOGGER.warn("--------------------------------------------------------------------------------");
            LOGGER.warn("| WARNING: Cassandra is configured to write data into the mesos sandbox");
            LOGGER.warn("--------------------------------------------------------------------------------");
            return ".";
        }
    };

    private static final Supplier<String> DEFAULT_BACKUP_DIRECTORY = new Supplier<String>() {
        @Override
        public String get() {
            LOGGER.warn("--------------------------------------------------------------------------------");
            LOGGER.warn("| WARNING: Cassandra is configured to write backup data into the mesos sandbox");
            LOGGER.warn("--------------------------------------------------------------------------------");
            return "./backup";
        }
    };

    @Language("RegExp")
    private static final String userAndPass     = "[^/@]+";
    @Language("RegExp")
    private static final String hostAndPort     = "[A-z0-9-.]+(?::\\d+)?";
    @Language("RegExp")
    private static final String zkNode          = "[^/]+";
    @Language("RegExp")
    private static final String REGEX = "^zk://((?:" + userAndPass + "@)?(?:" + hostAndPort + "(?:," + hostAndPort + ")*))(/" + zkNode + "(?:/" + zkNode + ")*)$";
    private static final String validZkUrl = "zk://host1:port1,host2:port2,.../path";
    private static final Pattern zkURLPattern = Pattern.compile(REGEX);

    public static void main(final String[] args) {
        int status;
        try {
            final Handler[] handlers = LogManager.getLogManager().getLogger("").getHandlers();
            for (final Handler handler : handlers) {
                handler.setLevel(Level.OFF);
            }
            org.slf4j.LoggerFactory.getLogger("slf4j-logging").debug("Installing SLF4JLogging");
            SLF4JBridgeHandler.install();
            status = _main();
        } catch (final SystemExitException e) {
            LOGGER.error(e.getMessage());
            status = e.status;
        } catch (final UnknownHostException e) {
            LOGGER.error("Unable to resolve local interface for http server");
            status = 6;
        } catch (final Throwable e) {
            LOGGER.error("Unhandled fatal exception", e);
            status = 10;
        }

        System.exit(status);
    }

    private static int _main() throws UnknownHostException {
        final Optional<String> portOption = Env.option("PORT0");
        if (!portOption.isPresent()) {
            throw new SystemExitException("Environment variable PORT0 must be defined", 5);
        }

        final int port0 = Integer.parseInt(portOption.get());
        final String host = Env.option("HOST").or("localhost");

        final Optional<String> clusterNameOpt = Env.option("CASSANDRA_CLUSTER_NAME");

        final int       executorCount               = Integer.parseInt(     Env.option("CASSANDRA_NODE_COUNT").or("3"));
        final int       seedCount                   = Integer.parseInt(     Env.option("CASSANDRA_SEED_COUNT").or("2"));
        final double    resourceCpuCores            = Double.parseDouble(   Env.option("CASSANDRA_RESOURCE_CPU_CORES").or("2.0"));
        final long      resourceMemoryMegabytes     = Long.parseLong(       Env.option("CASSANDRA_RESOURCE_MEM_MB").or("2048"));
        final long      resourceDiskMegabytes       = Long.parseLong(       Env.option("CASSANDRA_RESOURCE_DISK_MB").or("2048"));
        final long      javaHeapMb                  = Long.parseLong(       Env.option("CASSANDRA_RESOURCE_HEAP_MB").or("0"));
        final long      healthCheckIntervalSec      = Long.parseLong(       Env.option("CASSANDRA_HEALTH_CHECK_INTERVAL_SECONDS").or("60"));
        final long      bootstrapGraceTimeSec       = Long.parseLong(       Env.option("CASSANDRA_BOOTSTRAP_GRACE_TIME_SECONDS").or("120"));
        final String    cassandraVersion            =                       "2.1.4";
        final String    clusterName                 =                       clusterNameOpt.or("cassandra");
        final String    frameworkName               =                       frameworkName(clusterNameOpt);
        final String    zkUrl                       =                       Env.option("CASSANDRA_ZK").or("zk://localhost:2181/cassandra-mesos");
        final long      zkTimeoutMs                 = Long.parseLong(       Env.option("CASSANDRA_ZK_TIMEOUT_MS").or("10000"));
        final String    mesosMasterZkUrl            =                       Env.option("MESOS_ZK").or("zk://localhost:2181/mesos");
        final String    mesosUser                   =                       Env.option("MESOS_USER").or("");
        final long      failoverTimeout             = Long.parseLong(       Env.option("CASSANDRA_FAILOVER_TIMEOUT_SECONDS").or(String.valueOf(Period.days(7).toStandardSeconds().getSeconds())));
        final String    mesosRole                   =                       Env.option("CASSANDRA_FRAMEWORK_MESOS_ROLE").or("*");
        final String    dataDirectory               =                       Env.option("CASSANDRA_DATA_DIRECTORY").or(DEFAULT_DATA_DIRECTORY);  // TODO: Temporary. Will be removed when MESOS-1554 is released
        final String    backupDirectory             =                       Env.option("CASSANDRA_BACKUP_DIRECTORY").or(DEFAULT_BACKUP_DIRECTORY);
        final boolean   jmxLocal                    = Boolean.parseBoolean( Env.option("CASSANDRA_JMX_LOCAL").or("true"));
        final boolean   jmxNoAuthentication         = Boolean.parseBoolean( Env.option("CASSANDRA_JMX_NO_AUTHENTICATION").or("false"));
        final String    defaultRack                 =                       Env.option("CASSANDRA_DEFAULT_RACK").or("RAC1");
        final String    defaultDc                   =                       Env.option("CASSANDRA_DEFAULT_DC").or("DC1");

        final List<ExternalDc> externalDcs = getExternalDcs(Env.filterStartsWith("CASSANDRA_EXTERNAL_DC_", true));
        final Matcher matcher = validateZkUrl(zkUrl);

        final State state = new ZooKeeperState(
            matcher.group(1),
            zkTimeoutMs,
            TimeUnit.MILLISECONDS,
            matcher.group(2)
        );

        if (seedCount > executorCount || seedCount <= 0 || executorCount <= 0) {
            throw new IllegalArgumentException("number of nodes (" + executorCount + ") and/or number of seeds (" + seedCount + ") invalid");
        }

        final PersistedCassandraFrameworkConfiguration configuration = new PersistedCassandraFrameworkConfiguration(
            state,
            frameworkName,
            healthCheckIntervalSec,
            bootstrapGraceTimeSec,
            cassandraVersion,
            resourceCpuCores,
            resourceDiskMegabytes,
            resourceMemoryMegabytes,
            javaHeapMb,
            executorCount,
            seedCount,
            mesosRole,
            backupDirectory,
            dataDirectory,
            jmxLocal,
            jmxNoAuthentication,
            defaultRack,
            defaultDc,
            externalDcs,
            clusterName);


        final FrameworkInfo.Builder frameworkBuilder =
            FrameworkInfo.newBuilder()
                .setFailoverTimeout(failoverTimeout)
                .setUser(mesosUser)
                .setName(frameworkName)
                .setRole(mesosRole)
                .setCheckpoint(true);

        final Optional<String> frameworkId = configuration.frameworkId();
        if (frameworkId.isPresent()) {
            frameworkBuilder.setId(frameworkId(frameworkId.get()));
        }

        final URI httpServerBaseUri = URI.create("http://" + host + ":" + port0 + "/");

        final Clock clock = new SystemClock();
        final PersistedCassandraClusterHealthCheckHistory healthCheckHistory = new PersistedCassandraClusterHealthCheckHistory(state);
        final PersistedCassandraClusterState clusterState = new PersistedCassandraClusterState(state);
        final SeedManager seedManager = new SeedManager(configuration, new ObjectMapper(), new SystemClock());
        final CassandraCluster cassandraCluster = new CassandraCluster(
            clock,
            httpServerBaseUri.toString(),
            new ExecutorCounter(state, 0L),
            clusterState,
            healthCheckHistory,
            new PersistedCassandraClusterJobs(state),
            configuration,
            seedManager
        );
        final HealthReportService healthReportService = new HealthReportService(
            clusterState,
            configuration,
            healthCheckHistory,
            clock
        );
        final Scheduler scheduler = new CassandraScheduler(
            configuration,
            cassandraCluster,
            clock
        );

        final JsonFactory factory = new JsonFactory();
        final ObjectMapper objectMapper = new ObjectMapper(factory);
        objectMapper.registerModule(new GuavaModule());

        // create JsonProvider to provide custom ObjectMapper
        JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider();
        provider.setMapper(objectMapper);

        final ResourceConfig rc = new ResourceConfig()
            .registerInstances(
                new FileResourceController(cassandraVersion),
                new ApiController(factory),
                new ClusterCleanupController(cassandraCluster, factory),
                new ClusterRepairController(cassandraCluster, factory),
                new ClusterRollingRestartController(cassandraCluster, factory),
                new ClusterBackupController(cassandraCluster, factory),
                new ClusterRestoreController(cassandraCluster, factory),
                new ConfigController(cassandraCluster, factory),
                new LiveEndpointsController(cassandraCluster, factory),
                new NodeController(cassandraCluster, factory),
                new HealthCheckController(healthReportService),
                new QaReportController(cassandraCluster, factory),
                new ScaleOutController(cassandraCluster, factory),
                provider
            );
        final HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer(httpServerBaseUri, rc);

        final MesosSchedulerDriver driver;
        final Optional<Credential> credentials = getCredential();
        if (credentials.isPresent()) {
            frameworkBuilder.setPrincipal(credentials.get().getPrincipal());
            driver = new MesosSchedulerDriver(scheduler, frameworkBuilder.build(), mesosMasterZkUrl, credentials.get());
        } else {
            frameworkBuilder.setPrincipal("cassandra-framework");
            driver = new MesosSchedulerDriver(scheduler, frameworkBuilder.build(), mesosMasterZkUrl);
        }

        seedManager.startSyncingSeeds(60);

        final int status;
        switch (driver.run()) {
            case DRIVER_STOPPED:
                status = 0;
                break;
            case DRIVER_ABORTED:
                status = 1;
                break;
            case DRIVER_NOT_STARTED:
                status = 2;
                break;
            default:
                status = 3;
                break;
        }

        httpServer.shutdownNow();
        // Ensure that the driver process terminates.
        driver.stop(true);
        return status;
    }

    static List<ExternalDc> getExternalDcs(Map<String, String> dcOpts) {
        final List<ExternalDc> externalDcs = newArrayList();

        for (final String key: dcOpts.keySet()) {
            externalDcs.add(
                ExternalDc.newBuilder()
                    .setName(key)
                    .setUrl(dcOpts.get(key))
                    .build()
            );
        }
        return externalDcs;
    }

    static Matcher validateZkUrl(final String zkUrl) {
        final Matcher matcher = zkURLPattern.matcher(zkUrl);

        if (!matcher.matches()) {
            throw new SystemExitException(String.format("Invalid zk url format: '%s' expected '%s'", zkUrl, validZkUrl), 7);
        }
        return matcher;
    }

    static String frameworkName(final Optional<String> clusterName) {
        if (clusterName.isPresent()) {
            return "cassandra." + clusterName.get();
        } else {
            return "cassandra";
        }
    }

    static Optional<Credential> getCredential() {
        final boolean auth = Boolean.valueOf(Env.option("MESOS_AUTHENTICATE").or("false"));
        if (auth){
            LOGGER.info("Enabling authentication for the framework");

            final String principal = Env.get("DEFAULT_PRINCIPAL");
            final Optional<String> secret = Env.option("DEFAULT_SECRET");

            return Optional.of(ProtoUtils.getCredential(principal, secret));
        } else {
            return Optional.absent();
        }
    }

    static class SystemExitException extends RuntimeException {
        private final int status;

        public SystemExitException(final String message, final int status) {
            super(message);
            this.status = status;
        }
    }
}