package org.opennms.newts.graphite;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.opennms.newts.api.Timestamp.fromEpochSeconds;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;

import org.opennms.newts.api.MetricType;
import org.opennms.newts.api.Resource;
import org.opennms.newts.api.Sample;
import org.opennms.newts.api.SampleRepository;
import org.opennms.newts.api.Timestamp;
import org.opennms.newts.api.ValueType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

public class GraphiteHandler extends SimpleChannelInboundHandler<String> {

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

    private static final int DEFAULT_LINES_BUFFER = 50;

    private final ThreadPoolExecutor m_executor;
    private final SampleRepository m_repository;
    private final GraphiteInitializer m_parent;

    private List<String> m_lines;
    private AtomicInteger m_enQueued = new AtomicInteger(0);

    public GraphiteHandler(SampleRepository repository, GraphiteInitializer parent) {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
        int concurrency = Runtime.getRuntime().availableProcessors();
        m_executor = new ThreadPoolExecutor(concurrency, concurrency, 0L, MILLISECONDS, queue);
        m_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        m_repository = repository;
        m_parent = parent;
        m_lines = Lists.newArrayList();
        LOG.debug("Using storage concurrency of {}", concurrency);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        enqueue(msg);
    }

    private void enqueue(String msg) {
        m_lines.add(msg);

        if (m_enQueued.incrementAndGet() >= DEFAULT_LINES_BUFFER) {
            final List<String> batch = m_lines;
            m_lines = Lists.newArrayList();
            m_enQueued.set(0);
            m_executor.execute(new Runnable() {

                @Override
                public void run() {
                    List<Sample> samples = Lists.newArrayList();
                    for (String line : batch) {
                        try {
                            samples.add(parseSample(line));
                        }
                        catch (Exception e) {
                            m_parent.protocolErrorsInc();
                        }
                    }
                    try {
                        m_repository.insert(samples);
                    }
                    catch (Exception e) {
                        LOG.warn("Unable to commit batch of {} samples ({})", samples.size(), e.getMessage());
                        m_parent.storageErrorsInc();
                    }
                }
            });
        }

    }

    private static final Splitter s_lineTokenizer = Splitter.on(CharMatcher.whitespace()).limit(3).trimResults();
    private static final Splitter s_pathTokenizer = Splitter.on('.').trimResults();
    private static final Joiner s_pathJoiner = Joiner.on(':');

    static Resource parseResource(String[] path) {
        Map<String, String> attributes = Maps.newHashMap();
        for (int i = 0; i < path.length; i++) {
            attributes.put(index(i), path[i]);
        }
        return new Resource(s_pathJoiner.join(path), Optional.of(attributes));
    }

    static Sample parseSample(String line) {
        List<String> parts = s_lineTokenizer.splitToList(line);
        String[] path = s_pathTokenizer.splitToList(parts.get(0)).toArray(new String[] {});
        Resource resource = parseResource(path.length > 1 ? Arrays.copyOf(path, path.length - 1) : path);
        String name = path.length > 1 ? path[path.length - 1] : "value";
        Double value = Double.parseDouble(parts.get(1));
        Long stamp = Long.parseLong(parts.get(2));

        return sample(fromEpochSeconds(stamp), resource, name, value);
    }

    private static Sample sample(Timestamp timestamp, Resource resource, String name, Double value) {
        return new Sample(timestamp, resource, name, MetricType.GAUGE, ValueType.compose(value, MetricType.GAUGE));
    }

    private static String index(int index) {
        return String.format("_%d", index);
    }

}
