package resa.topology;

import backtype.storm.Config;
import backtype.storm.generated.Nimbus;
import backtype.storm.generated.RebalanceOptions;
import backtype.storm.generated.StormTopology;
import backtype.storm.metric.api.IMetricsConsumer;
import backtype.storm.scheduler.ExecutorDetails;
import backtype.storm.task.IErrorReporter;
import backtype.storm.task.TopologyContext;
import backtype.storm.utils.NimbusClient;
import backtype.storm.utils.Utils;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import resa.drs.ResourceScheduler;
import resa.metrics.FilteredMetricsCollector;
import resa.metrics.MeasuredData;
import resa.metrics.MetricNames;
import resa.util.ConfigUtil;
import resa.util.TopologyHelper;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.stream.Collectors;

import static resa.util.ResaConfig.REBALANCE_WAITING_SECS;

/**
 * Created by ding on 14-5-5.
 */
public class ResaContainer extends FilteredMetricsCollector {

    public static final String REDIS_HOST = "resa.container.metric.redis.host";
    public static final String REDIS_PORT = "resa.container.metric.redis.port";
    public static final String REDIS_QUEUE_NAME = "resa.container.metric.redis.queue-name";
    public static final String METRIC_OUTPUT = "resa.container.metric.output";

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

    private ResourceScheduler resourceScheduler = new ResourceScheduler();
    private ContainerContext ctx;
    private Nimbus.Client nimbus;
    private String topologyName;
    private String topologyId;
    private Map<String, Object> conf;
    private Thread metricSendThread;
    private final BlockingQueue<String> metricsQueue = new LinkedBlockingDeque<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void prepare(Map conf, Object arg, TopologyContext context, IErrorReporter errorReporter) {
        super.prepare(conf, arg, context, errorReporter);
        this.conf = conf;
        this.topologyName = (String) conf.get(Config.TOPOLOGY_NAME);
        // connected to nimbus
        nimbus = NimbusClient.getConfiguredClient(conf).getClient();
        topologyId = TopologyHelper.getTopologyId(nimbus, topologyName);
        // add approved metric name
        addApprovedMetirc("__sendqueue", MetricNames.SEND_QUEUE);
        addApprovedMetirc("__receive", MetricNames.RECV_QUEUE);
        addApprovedMetirc(MetricNames.COMPLETE_LATENCY);
        addApprovedMetirc(MetricNames.TASK_EXECUTE);
        addApprovedMetirc(MetricNames.EMIT_COUNT);
        addApprovedMetirc(MetricNames.DURATION);

        metricSendThread = createMetricsSendThread();
        metricSendThread.start();
        LOG.info("Metrics send thread started");
        ctx = new ContainerContextImpl(context.getRawTopology(), conf);
        // topology optimizer will start its own thread
        // if more services required to start, maybe we need to extract a new interface here
        resourceScheduler.init(ctx);
        resourceScheduler.start();

        outputTopologyInfo(context);
        if ((Boolean) conf.getOrDefault(METRIC_OUTPUT, Boolean.FALSE)) {
            outputMetric();
        }
    }

    private void outputTopologyInfo(TopologyContext context) {
        Map<String, Object> topo = new LinkedHashMap<>();
        topo.put("id", context.getStormId());
        topo.put("spouts", new ArrayList<>(context.getRawTopology().get_spouts().keySet()));
        topo.put("bolts", new ArrayList<>(context.getRawTopology().get_bolts().keySet()));
        topo.put("targets", new HashMap<>());
        for (String comp : context.getComponentIds()) {
            Map<String, List<String>> targets = new HashMap<>();
            context.getTargets(comp).forEach((stream, g) -> {
                targets.put(stream, new ArrayList<>(g.keySet()));
            });
            ((Map<String, Object>) topo.get("targets")).put(comp, targets);
        }
        ctx.emitMetric("topology.info", topo);
    }

    private void outputMetric() {
        ctx.addListener(new ContainerContext.Listener() {
            @Override
            public void measuredDataReceived(MeasuredData measuredData) {
                String key = "task." + measuredData.component + "." + measuredData.task;
                ctx.emitMetric(key, measuredData.data);
            }
        });
    }

    private String object2Json(Object o) {
        try {
            return objectMapper.writeValueAsString(o);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Thread createMetricsSendThread() {
        String jedisHost = (String) conf.get(REDIS_HOST);
        int jedisPort = ((Number) conf.get(REDIS_PORT)).intValue();
        String queueName = (String) conf.get(REDIS_QUEUE_NAME);
        // queue name is not exist, use topology id as default
        if (queueName == null || queueName.isEmpty()) {
            queueName = topologyId + "-container";
        }
        final String finalQueueName = queueName;
        Thread t = new Thread("Metrics send thread") {
            private Jedis jedis;

            @Override
            public void run() {
                String value = null;
                while (true) {
                    if (value == null) {
                        try {
                            value = metricsQueue.take();
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                    try {
                        getJedisInstance(jedisHost, jedisPort).rpush(finalQueueName, value);
                        value = null;
                    } catch (Exception e) {
                        closeJedis();
                        Utils.sleep(1);
                    }
                }
                closeJedis();
                LOG.info("Metrics send thread exit");
            }

            /* get a jedis instance, create a one if necessary */
            private Jedis getJedisInstance(String jedisHost, int jedisPort) {
                if (jedis == null) {
                    jedis = new Jedis(jedisHost, jedisPort);
                    LOG.info("connecting to redis server {} on port {}", jedisHost, jedisPort);
                }
                return jedis;
            }

            private void closeJedis() {
                if (jedis != null) {
                    try {
                        LOG.info("disconnecting redis server " + jedisHost);
                        jedis.disconnect();
                    } catch (Exception e) {
                    }
                    jedis = null;
                }
            }
        };
        t.setDaemon(true);
        return t;
    }

    private class ContainerContextImpl extends ContainerContext {

        protected ContainerContextImpl(StormTopology topology, Map<String, Object> conf) {
            super(topology, conf);
        }

        @Override
        public void emitMetric(String name, Object data) {
            String val = name + "->" + object2Json(data);
            metricsQueue.add(val);
        }

        @Override
        public Map<String, List<ExecutorDetails>> runningExecutors() {
            return TopologyHelper.getTopologyExecutors(nimbus, topologyId).entrySet().stream()
                    .filter(e -> !Utils.isSystemId(e.getKey()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        }

        @Override
        public boolean requestRebalance(Map<String, Integer> allocation, int numWorkers) {
            RebalanceOptions options = new RebalanceOptions();
            //set rebalance options
            options.set_num_workers(numWorkers);
            options.set_num_executors(allocation);
            int waitingSecs = ConfigUtil.getInt(conf, REBALANCE_WAITING_SECS, -1);
            if (waitingSecs >= 0) {
                options.set_wait_secs(waitingSecs);
            }
            try {
                nimbus.rebalance(topologyName, options);
                LOG.info("do rebalance successfully for topology " + topologyName);
                return true;
            } catch (Exception e) {
                LOG.warn("do rebalance failed for topology " + topologyName, e);
            }
            return false;
        }
    }

    @Override
    protected void handleSelectedDataPoints(IMetricsConsumer.TaskInfo taskInfo,
                                            Collection<IMetricsConsumer.DataPoint> dataPoints) {
        Map<String, Object> ret = dataPoints.stream().collect(Collectors.toMap(p -> p.name, p -> p.value));
        MeasuredData measuredData = new MeasuredData(taskInfo.srcComponentId, taskInfo.srcTaskId,
                taskInfo.timestamp, ret);
        ctx.getListeners().forEach(l -> l.measuredDataReceived(measuredData));
    }

    @Override
    public void cleanup() {
        super.cleanup();
        resourceScheduler.stop();
        metricsQueue.clear();
        metricSendThread.interrupt();
    }
}
