package org.cloudfoundry.promregator.textformat004;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.prometheus.client.Collector;
import io.prometheus.client.Collector.MetricFamilySamples;
import io.prometheus.client.Collector.MetricFamilySamples.Sample;
import io.prometheus.client.Collector.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/* Unfortunately, there is nothing provided for this by Prometheus.io :-(
 * So, we have to do this ourselves.
 * Details of the format are described at https://prometheus.io/docs/instrumenting/exposition_formats/
 */
public class Parser {
	private static final Logger log = LoggerFactory.getLogger(Parser.class);
	
	private String textFormat004data;
	
	private HashMap<String, String> mapHelps = new HashMap<>();
	private HashMap<String, Collector.Type> mapTypes = new HashMap<>();
	
	private HashMap<String, Collector.MetricFamilySamples> mapMFS = new HashMap<>();
	
	private static final Pattern PATTERN_HELP = Pattern.compile("^#[ \t]+HELP[ \t]+");
	private static final Pattern PATTERN_TYPE = Pattern.compile("^#[ \t]+TYPE[ \t]+");
	private static final Pattern PATTERN_COMMENT = Pattern.compile("^#");
	private static final Pattern PATTERN_EMPTYLINE = Pattern.compile("^[ \t]*$");
	
	private static final Pattern PATTERN_PARSE_HELP = Pattern.compile("^#[ \t]+HELP[ \t]+([a-zA-Z0-9:_\\\"]+)[ \\t]+(.*)$");
	private static final Pattern PATTERN_PARSE_TYPE = Pattern.compile("^#[ \t]+TYPE[ \t]+([a-zA-Z0-9:_\\\"]+)[ \\t]+([a-zA-Z]*)$");
	
	public Parser(String textFormat004data) {
		this.textFormat004data = textFormat004data;
	}
	
	public HashMap<String, Collector.MetricFamilySamples> parse() {
		this.reset();
		
		StringTokenizer lines = new StringTokenizer(this.textFormat004data, "\n");
		
		while(lines.hasMoreTokens()) {
			String line = lines.nextToken();
			
			// warning! Order of IF tests matter!
			if (this.isEmptyLine(line)) {
				continue;
			} else if (this.isHelpLine(line)) {
				this.parseHelpLine(line);
				continue;
			} else if (this.isTypeLine(line)) {
				this.parseTypeLine(line);
				continue;
			} else if (this.isCommentLine(line)) {
				continue;
			}
			
			// we need to assume that this is a metric line
			this.parseMetric(line);
		}
		
		return this.mapMFS;
	}
	
	private void parseMetric(String line) {
		final MetricLine ml = new MetricLine(line);
		
		Sample sample = null;
		try {
			sample = ml.parse();
		} catch (MetricLine.ParseException e) {
			log.warn(String.format("Detected non-parsable metric line '%s'", line), e);
			return;
		}
		
		final String metricName = sample.name;
		
		Collector.Type type = determineType(metricName);
		if (type == Type.UNTYPED) {
			log.info(String.format("Definition of metric %s without type information (assuming untyped)", metricName));
		}
		
		if (type.equals(Collector.Type.COUNTER) || type.equals(Collector.Type.GAUGE) || type.equals(Collector.Type.UNTYPED)) {
			this.storeSimpleType(sample, metricName, type);
		} else if (type.equals(Collector.Type.HISTOGRAM) || type.equals(Collector.Type.SUMMARY)) {
			this.storeComplexType(sample, metricName, type);
		} else {
			log.warn(String.format("Unknown type %s; unclear how to handle this; skipping", type.toString()));
			// return; can be skipped here
		}
	}

	private void storeSimpleType(Sample sample, final String metricName, Collector.Type type) {
		MetricFamilySamples mfsStored = this.mapMFS.get(metricName);
		if (mfsStored != null) {
			// we already have created a metric for this line; we just have to add the sample
			mfsStored.samples.add(sample);
		} else {
			// there is no such MFS entry yet; we have to create one
			List<Sample> samples = new LinkedList<>();
			samples.add(sample);

			String docString = this.mapHelps.get(metricName);
			/*
			 * mfs.help must not be empty - see also  https://github.com/promregator/promregator/issues/73
			 */
			if (docString == null) {
				docString = "";
			}

			Collector.MetricFamilySamples mfs = new Collector.MetricFamilySamples(metricName, type, docString, samples);
			this.mapMFS.put(metricName, mfs);
		}
	}

	private void storeComplexType(Sample sample, final String metricName, Collector.Type type) {
		String baseMetricName = determineBaseMetricName(metricName);

		// is this already in our Map?
		Collector.MetricFamilySamples mfs = this.mapMFS.get(baseMetricName);
		if (mfs == null) {
			// no, we have to create a new one
			
			String docString = this.mapHelps.get(baseMetricName);
			/*
			 * mfs.help must not be empty - see also  https://github.com/promregator/promregator/issues/73
			 */
			if (docString == null) {
				docString = "";
			}
			
			mfs = new Collector.MetricFamilySamples(baseMetricName, type, docString, new LinkedList<>());
			this.mapMFS.put(baseMetricName, mfs);
		}
		
		mfs.samples.add(sample);
	}
	
	private Type determineType(String metricName) {
		Collector.Type type = null;
		// first check if the metric is typed natively.
		type = this.mapTypes.get(metricName);
		if (type != null) {
			return type;
		}
		
		// try to get the baseMetricName
		String baseMetricName = determineBaseMetricName(metricName);
		type = this.mapTypes.get(baseMetricName);
		// check that this also really makes sense and is a type, which requires baseMetricNames
		if (type == Type.HISTOGRAM || type == Type.SUMMARY) {
			return type;
		}
		
		// we have no clue what this metric is all about
		return Collector.Type.UNTYPED;
	}

	private static String determineBaseMetricName(String metricName) {
		if (metricName.endsWith("_bucket")) {
			return metricName.substring(0, metricName.length()-7);
		} else if (metricName.endsWith("_sum")) {
			return metricName.substring(0, metricName.length()-4);
		} else if (metricName.endsWith("_count")) {
			return metricName.substring(0, metricName.length()-6);
		} else if (metricName.endsWith("_max")) {
			// provided as additional metric by micrometer
			return metricName.substring(0, metricName.length()-4);
		}
		
		return metricName;
	}
	
	private void parseTypeLine(String line) {
		Matcher m = PATTERN_PARSE_TYPE.matcher(line);
		if (!m.matches()) {
			log.warn("TYPE line could not be properly matched: "+line);
			return;
		}
		
		String metricName = Utils.unescapeToken(m.group(1));
		String typeString = m.group(2);
		
		Collector.Type type = null;
		if (typeString.equalsIgnoreCase("gauge")) {
			type = Collector.Type.GAUGE;
		} else if (typeString.equalsIgnoreCase("counter")) {
			type = Collector.Type.COUNTER;
		} else if (typeString.equalsIgnoreCase("summary")) {
			type = Collector.Type.SUMMARY;
		} else if (typeString.equalsIgnoreCase("histogram")) {
			type = Collector.Type.HISTOGRAM;
		} else if (typeString.equalsIgnoreCase("untyped")) {
			type = Collector.Type.UNTYPED;
		} else {
			log.warn("Unable to parse type from TYPE line: "+line);
			return;
		}
		
		this.mapTypes.put(metricName, type);
	}

	private void parseHelpLine(String line) {
		Matcher m = PATTERN_PARSE_HELP.matcher(line);
		if (!m.matches()) {
			log.warn("HELP line could not be properly matched: "+line);
			return;
		}
		
		String metricName = Utils.unescapeToken(m.group(1));
		String docString = unescapeDocString(m.group(2));
		
		this.mapHelps.put(metricName, docString);
	}

	private boolean isHelpLine(String line) {
		return PATTERN_HELP.matcher(line).find();
	}
	
	private boolean isTypeLine(String line) {
		return PATTERN_TYPE.matcher(line).find();
	}

	private boolean isCommentLine(String line) {
		return PATTERN_COMMENT.matcher(line).find();
	}

	private boolean isEmptyLine(String line) {
		return PATTERN_EMPTYLINE.matcher(line).find();
	}
	
	private String unescapeDocString(String s) {
		if (s == null)
			return null;
		
		String sTemp = s.replace("\\\\", "\\");
		sTemp = sTemp.replace("\\n", "\n");
		
		return sTemp;
	}
	
	private void reset() {
		this.mapHelps.clear();
		this.mapTypes.clear();
		this.mapMFS.clear();
	}
	
}