/*
 * Tigase AuditLog Component - Implementation of Audit Log pattern for Tigase XMPP Server
 * Copyright (C) 2017 Tigase, Inc. (office@tigase.com) - All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 */
/*
 Get AuditLog entries

 AS:Description: Get connections history
 AS:CommandId: get-connections-history
 AS:Component: audit-log
 AS:Group: Audit Log
 */

package tigase.admin

import groovy.transform.CompileStatic
import tigase.auditlog.AuditLogComponent
import tigase.auditlog.LogSearchableRepository
import tigase.server.Command
import tigase.server.DataForm
import tigase.server.Iq
import tigase.server.Packet
import tigase.util.datetime.TimestampHelper
import tigase.xml.Element
import tigase.xmpp.jid.BareJID

import java.time.Instant
import java.time.LocalDateTime
import java.time.Period
import java.time.temporal.ChronoUnit
import java.util.stream.Collectors

AuditLogComponent component = (AuditLogComponent) component
packet = (Iq) packet

@CompileStatic
String formatDuration(Date ts, Double duration) {
	if (duration == null) {
		return null;
	}

	if (ts == null) {
		ts = new Date();
	}
	
	Instant from = Instant.ofEpochMilli(ts.getTime() - ((long)(duration * 1000)));
	Instant to = ts.toInstant();

	StringBuilder sb = new StringBuilder();
	long amount = from.until(to, ChronoUnit.DAYS);
	if (amount) {
		from = from.plus(amount, ChronoUnit.DAYS);
		if (sb.length() > 0) {
			sb.append(" ");
		}
		sb.append(amount).append("d");
	}
	amount = from.until(to, ChronoUnit.HOURS);
	if (amount) {
		from = from.plus(amount, ChronoUnit.HOURS);
		if (sb.length() > 0) {
			sb.append(" ");
		}
		sb.append(amount).append("h");
	}
	amount = from.until(to, ChronoUnit.MINUTES);
	if (amount) {
		from = from.plus(amount, ChronoUnit.MINUTES);
		if (sb.length() > 0) {
			sb.append(" ");
		}
		sb.append(amount).append("m");
	}
	amount = from.until(to, ChronoUnit.SECONDS);
	if (amount) {
		from = from.plus(amount, ChronoUnit.SECONDS);
		if (sb.length() > 0) {
			sb.append(" ");
		}
		sb.append(amount).append("s");
	}
	amount = from.until(to, ChronoUnit.MILLIS);
	if (amount) {
		from = from.plus(amount, ChronoUnit.MILLIS);
		if (sb.length() > 0) {
			sb.append(" ");
		}
		sb.append(amount).append("ms");
	}
	if (sb.length() == 0) {
		return "0ms";
	}
	return sb.toString();
}

@CompileStatic
Element createDurationValueElem(Date ts, Double duration) {
	Element value = new Element("value", duration != null ? String.format("%.3f", duration) : "");
	if (duration != null) {
		value.addAttribute("label", formatDuration(ts, duration));
	}
	return value;
}

@CompileStatic
Element createTimestampValueElement(TimestampHelper timestampHelper, Date ts) {
	Element value = new Element("value", ts ? timestampHelper.formatWithMs(ts) : "");
	if (ts != null) {
		value.addAttribute("label", "" + new java.sql.Timestamp(ts.getTime()));
	}
	return value;
}

@CompileStatic
void addCurrentStateEntry(Packet result, String state, TimestampHelper timestampHelper, LogSearchableRepository.ConnectionHistory con) {
	Element item = new Element("item");

	Element sess = new Element("field");
	sess.setAttribute("var", "Session");
	sess.addChild(new Element("value", con.getSessionId()));
	item.addChild(sess);

	Element res = new Element("field");
	res.setAttribute("var", "State");
	res.addChild(new Element("value", state));
	item.addChild(res);

	Element connFrom = new Element("field");
	connFrom.setAttribute("var", "Connected from");
	if (state != "Connected") {
		connFrom.addChild(createTimestampValueElement(timestampHelper, new java.sql.Timestamp(con.getTimestamp().getTime() - ((long)(con.getDuration() * 1000)))));
	} else {
		connFrom.addChild(createTimestampValueElement(timestampHelper, con.getTimestamp()));
	}
	item.addChild(connFrom);

	Element disconnFrom = new Element("field");
	disconnFrom.setAttribute("var", "Disconnected from");
	if (state != "Connected") {
		disconnFrom.addChild(createTimestampValueElement(timestampHelper, con.getTimestamp()))
	}
	item.addChild(disconnFrom);

	Element ip = new Element("field");
	ip.setAttribute("var", "Connected from");
	ip.addChild(new Element("value", con.getClientIP()));
	item.addChild(ip);

	Element duration = new Element("field");
	duration.setAttribute("var", "Connection duration");
	if (con.getDuration()) {
		duration.addChild(createDurationValueElem(null, con.getDuration()));
	}
	item.addChild(duration);

	result.getElement().getChild('command').getChild('x').addChild(item);
}

@CompileStatic
void addCurrentState(AuditLogComponent component, TimestampHelper timestampHelper, BareJID jid, Packet result) {
	Element reported = new Element("reported");
	reported.addAttribute("label", "Current state");
	def cols = [ "Session", "State", "Connected from", "Disconnected from", "IP", "Connection duration" ];
	cols.each {
		Element el = new Element("field");
		el.setAttribute("var", it);
		if (it == "Connection duration") {
			el.setAttribute("align", "right");
		}
		reported.addChild(el);
	}
	result.getElement().getChild('command').getChild('x').addChild(reported);
	List<LogSearchableRepository.ConnectionHistory> activeConnections = component.getSearchableRepository().getActiveConnections(jid);
	if (!activeConnections.isEmpty()) {
		activeConnections.each { con ->
			addCurrentStateEntry(result, "Connected", timestampHelper, con);
		}
	} else {
		component.getSearchableRepository().getLastConnections(jid).each { con ->
			addCurrentStateEntry(result, "Disconnected", timestampHelper, con);
		}
	}
}

@CompileStatic
void addStatistics(AuditLogComponent component, BareJID jid, Date from, Date to, Packet result) {
	Element reported = new Element("reported");
	reported.addAttribute("label", "Statistics");
	def cols = [ "Number of disconnections", "Number of connections", "Avg connection duration", "Number of connection failures" ];
	cols.each {
		Element el = new Element("field");
		el.setAttribute("var", it);
		el.setAttribute("align", "right");
		reported.addChild(el);
	}
	result.getElement().getChild('command').getChild('x').addChild(reported);
	LogSearchableRepository.ConnectionStatistics stats = component.getSearchableRepository().getConnectionStatistics(jid, from, to);
	if (stats != null) {
		Element item = new Element("item");
		Element numOfDisconn = new Element("field");
		numOfDisconn.setAttribute("var", "Number of disconnections");
		numOfDisconn.addChild(new Element("value", "" + stats.numberOfDisconnections));
		item.addChild(numOfDisconn);

		Element numOfConn = new Element("field");
		numOfConn.setAttribute("var", "Number of connections");
		numOfConn.addChild(new Element("value", "" + stats.numberOfConnections));
		item.addChild(numOfConn);

		Element avgDuration = new Element("field");
		avgDuration.setAttribute("var", "Avg connection duration");
		avgDuration.addChild(createDurationValueElem(null, stats.avgConnectionDuration));
		item.addChild(avgDuration);

		Element numOfFailures = new Element("field");
		numOfFailures.setAttribute("var", "Number of connection failures");
		numOfFailures.addChild(new Element("value", "" + stats.numberOfConnectionFailures));
		item.addChild(numOfFailures);

		result.getElement().getChild('command').getChild('x').addChild(item);
	}
}

@CompileStatic
void addConnectionsHistory(AuditLogComponent component, TimestampHelper timestampHelper, BareJID jid, Date from, Date to, Packet result) {
	Element reported = new Element("reported");
	reported.addAttribute("label", "History of connections");
	def cols = [ "Session", "Timestamp", "Action", /*"Action subtype",*/ "IP", "Connection duration", /*"Error condition",*/ "Error message" ];
	cols.each {
		Element el = new Element("field");
		el.setAttribute("var", it);
		if (it == "Connection duration") {
			el.setAttribute("align", "right");
		}
		reported.addChild(el);
	}
	result.getElement().getChild('command').getChild('x').addChild(reported);

	component.getSearchableRepository().getConnectionHistory(jid, from, to).each { 
		Element item = new Element("item");

		Element sess = new Element("field");
		sess.setAttribute("var", "Session");
		sess.addChild(new Element("value", it.sessionId));
		item.addChild(sess);

		Element ts = new Element("field");
		ts.setAttribute("var", "Timestamp");
		ts.addChild(createTimestampValueElement(timestampHelper, it.getTimestamp()));
		item.addChild(ts);

//		Element actionType = new Element("field");
//		actionType.setAttribute("var", "Action type");
//		actionType.addChild(new Element("value", "" + it.type));
//		item.addChild(actionType);
//
//		Element actionSubType = new Element("field");
//		actionSubType.setAttribute("var", "Action subtype");
//		actionSubType.addChild(new Element("value", "" + it.subtype));
//		item.addChild(actionSubType);

		Element actionSubType = new Element("field");
		actionSubType.setAttribute("var", "Action");
		String actionName = "";
		switch (it.getSubtype()) {
			case "success":
				actionName = "Connected";
				break;
			case "failure":
				actionName = "Authentication error";
				break;
			case "disconnected":
				actionName = "Disconnected";
				break;
			default:
				actionName = "UNKNOWN";
				break;
		}
		actionSubType.addChild(new Element("value", actionName));
		item.addChild(actionSubType);

		Element ip = new Element("field");
		ip.setAttribute("var", "IP");
		ip.addChild(new Element("value", "" + it.clientIP));
		item.addChild(ip);

		Element duration = new Element("field");
		duration.setAttribute("var", "Connection duration");
		duration.addChild(createDurationValueElem(it.timestamp, it.duration));
		item.addChild(duration);

//		Element errorCondition = new Element("field");
//		errorCondition.setAttribute("var", "Error condition");
//		errorCondition.addChild(new Element("value", "" + it.errorConditon));
//		item.addChild(errorCondition);

		Element errorMessage = new Element("field");
		errorMessage.setAttribute("var", "Error message");
		if (it.errorMessage) {
			errorMessage.addChild(new Element("value", it.errorMessage));
		}
		item.addChild(errorMessage);

		result.getElement().getChild('command').getChild('x').addChild(item);
	}
}

@CompileStatic
Packet process(AuditLogComponent component, Iq p) {
	String JID = "jid";
	String FROM = "from";
	String TO = "to";

	TimestampHelper timestampHelper = new TimestampHelper();

	String jidStr = Command.getFieldValue(p, "jid");
	BareJID jid = jidStr ? BareJID.bareJIDInstance(jidStr) : null;
	Date from = timestampHelper.parseTimestamp(Command.getFieldValue(p, FROM)) ?: new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
	Date to = timestampHelper.parseTimestamp(Command.getFieldValue(p, TO)) ?: new Date();

	Packet result = p.commandResult(Command.DataType.form);

	Command.addTitle(result, "Querying auditlog repository.")
	Command.addInstructions(result, "Fill out this form to query auditlog repository.")

	Command.addFieldValue(result, JID, jid ? jid.toString() : "", "jid-single",
						  "JID")
	Command.addFieldValue(result, FROM, from ? timestampHelper.format(from) : "", "text-single",
						  "From")
	Command.addFieldValue(result, TO, to ? timestampHelper.format(to) : "", "text-single", "To");

	result.getElement().getChild(Command.COMMAND_EL).getChild("x", "jabber:x:data").getChildren().
			findAll { it -> it.getName() == "field" && [TO, FROM].contains(it.getAttribute("var")) }.
			each { field -> field.setAttribute("subtype", "datetime")};

	if (jid != null) {
		addCurrentState(component, timestampHelper, jid, result);
		addStatistics(component, jid, from, to, result);
		addConnectionsHistory(component, timestampHelper, jid, from, to, result);
	}

	return result
}

try {
	return process(component, packet)
} catch (Throwable ex) {
	ex.printStackTrace();
	throw ex;
}