package com.reucon.openfire.plugin.archive.xep0313;

import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;

import org.dom4j.*;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.muc.MultiUserChatService;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.*;

import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.xep.AbstractIQHandler;
import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;

/**
 * XEP-0313 IQ Query Handler
 */
abstract class IQQueryHandler extends AbstractIQHandler implements
		ServerFeaturesProvider {

	private static final Logger Log = LoggerFactory.getLogger(IQHandler.class);
	protected final String NAMESPACE;

	private final XMPPDateTimeFormat xmppDateTimeFormat = new XMPPDateTimeFormat();

	IQQueryHandler(final String moduleName, final String namespace) {
		super(moduleName, "query", namespace);
		NAMESPACE = namespace;
	}

	public IQ handleIQ(IQ packet) throws UnauthorizedException {

		Session session = sessionManager.getSession(packet.getFrom());

		// If no session was found then answer with an error (if possible)
        if (session == null) {
            Log.error("Error during resource binding. Session not found in " +
                    sessionManager.getPreAuthenticatedKeys() +
                    " for key " +
                    packet.getFrom());
            return buildErrorResponse(packet);
        }

		if(packet.getType().equals(IQ.Type.get)) {
			return buildSupportedFieldsResult(packet, session);
		}

		// Default to user's own archive
		JID archiveJid = packet.getTo();
		if (archiveJid == null) {
			archiveJid = packet.getFrom().asBareJID();
		}
		Log.debug("Archive requested is {}", archiveJid);

		// Now decide the type.
		boolean muc = false;
		if (!XMPPServer.getInstance().isLocal(archiveJid)) {
			Log.debug("Archive is not local (user)");
			if (XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(archiveJid) == null) {
				Log.debug("No chat service for this domain");
				return buildErrorResponse(packet);
			} else {
				muc = true;
				Log.debug("MUC");
			}
		}

		JID requestor = packet.getFrom().asBareJID();
		Log.debug("Requestor is {} for muc=={}", requestor, muc);

		// Auth checking.
		if(muc) {
			MultiUserChatService service = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(archiveJid);
			MUCRoom room = service.getChatRoom(archiveJid.getNode());
			if (room == null) {
				return buildErrorResponse(packet);
			}
			boolean pass = false;
			if (service.isSysadmin(requestor)) {
				pass = true;
			}
			MUCRole.Affiliation aff =  room.getAffiliation(requestor);
			if (aff != MUCRole.Affiliation.outcast) {
				if (aff == MUCRole.Affiliation.owner || aff == MUCRole.Affiliation.admin) {
					pass = true;
				} else if (room.isMembersOnly()) {
					if (aff == MUCRole.Affiliation.member) {
						pass = true;
					}
				} else {
					pass = true;
				}
			}
			if (!pass) {
				return buildForbiddenResponse(packet);
			}
		} else if(!archiveJid.equals(requestor)) { // Not user's own
			// ... disallow unless admin.
			if (!XMPPServer.getInstance().getAdmins().contains(requestor)) {
				return buildForbiddenResponse(packet);
			}
		}

		sendMidQuery(packet, session);

		final QueryRequest queryRequest = new QueryRequest(packet.getChildElement(), archiveJid);
		Collection<ArchivedMessage> archivedMessages = retrieveMessages(queryRequest);

		for(ArchivedMessage archivedMessage : archivedMessages) {
			sendMessageResult(session, queryRequest, archivedMessage);
		}

		sendEndQuery(packet, session, queryRequest);

		return null;
	}

	protected void sendMidQuery(IQ packet, Session session) {
		// Default: Do nothing.
	}

	protected abstract void sendEndQuery(IQ packet, Session session, QueryRequest queryRequest);

	/**
	 * Create error response to send to client
	 * @param packet IQ stanza received
	 * @return IQ stanza to be sent.
	 */
	private IQ buildErrorResponse(IQ packet) {
        IQ reply = IQ.createResultIQ(packet);
        reply.setChildElement(packet.getChildElement().createCopy());
        reply.setError(PacketError.Condition.internal_server_error);
        return reply;
	}

	/**
	 * Create error response due to forbidden request
	 * @param packet Received request
	 * @return
	 */
	private IQ buildForbiddenResponse(IQ packet) {
		IQ reply = IQ.createResultIQ(packet);
		reply.setChildElement(packet.getChildElement().createCopy());
		reply.setError(PacketError.Condition.forbidden);
		return reply;
	}

	/**
	 * Retrieve messages matching query request from server archive
	 * @param queryRequest
	 * @return
	 */
	private Collection<ArchivedMessage> retrieveMessages(QueryRequest queryRequest) {

		String withField = null;
		String startField = null;
		String endField = null;
		DataForm dataForm = queryRequest.getDataForm();
		if(dataForm != null) {
			if(dataForm.getField("with") != null) {
				withField = dataForm.getField("with").getFirstValue();
			}
			if(dataForm.getField("start") != null) {
				startField = dataForm.getField("start").getFirstValue();
			}
			if(dataForm.getField("end") != null) {
				endField = dataForm.getField("end").getFirstValue();
			}
		}

		Date startDate = null;
		Date endDate = null;
		try {
			if(startField != null) {
				startDate = xmppDateTimeFormat.parseString(startField);
			}
			if(endField != null) {
				endDate = xmppDateTimeFormat.parseString(endField);
			}
		} catch (ParseException e) {
			Log.error("Error parsing query date filters.", e);
		}

		return getPersistenceManager(queryRequest.getArchive()).findMessages(
				startDate,
				endDate,
				queryRequest.getArchive().toBareJID(),
				withField,
				queryRequest.getResultSet());
	}

	/**
	 * Send result packet to client acknowledging query.
	 * @param packet Received query packet
	 * @param session Client session to respond to
	 */
	private void sendAcknowledgementResult(IQ packet, Session session) {
		IQ result = IQ.createResultIQ(packet);
		session.process(result);
	}

	/**
	 * Send final message back to client following query.
	 * @param session Client session to respond to
	 * @param queryRequest Received query request
	 */
	private void sendFinalMessage(Session session,
			final QueryRequest queryRequest) {

		Message finalMessage = new Message();
		finalMessage.setTo(session.getAddress());
		Element fin = finalMessage.addChildElement("fin", NAMESPACE);
		if(queryRequest.getQueryid() != null) {
			fin.addAttribute("queryid", queryRequest.getQueryid());
		}

		XmppResultSet resultSet = queryRequest.getResultSet();
		if (resultSet != null) {
			fin.add(resultSet.createResultElement());

			if(resultSet.isComplete()) {
				fin.addAttribute("complete", "true");
			}
		}

		session.process(finalMessage);
	}

	/**
	 * Send archived message to requesting client
	 * @param session Client session that send message to
	 * @param queryRequest Query request made by client
	 * @param archivedMessage Message to send to client
	 * @return
	 */
	private void sendMessageResult(Session session,
			QueryRequest queryRequest, ArchivedMessage archivedMessage) {

		String stanzaText = archivedMessage.getStanza();
		if(stanzaText == null || stanzaText.equals("")) {
			// Try creating a fake one from the body.
			if (archivedMessage.getBody() != null && !archivedMessage.getBody().equals("")) {
				stanzaText = String.format("<message from=\"{}\" to=\"{}\" type=\"chat\"><body>{}</body>", archivedMessage.getWithJid(), archivedMessage.getWithJid(), archivedMessage.getBody());
			} else {
				// Don't send legacy archived messages (that have no stanza)
				return;
			}
		}

		Message messagePacket = new Message();
		messagePacket.setTo(session.getAddress());
		Forwarded fwd;

		Document stanza;
		try {
			stanza = DocumentHelper.parseText(stanzaText);
			fwd = new Forwarded(stanza.getRootElement(), archivedMessage.getTime(), null);
		} catch (DocumentException e) {
			Log.error("Failed to parse message stanza.", e);
			// If we can't parse stanza then we have no message to send to client, abort
			return;
		}

		if (fwd == null) return; // Shouldn't be possible.

		messagePacket.addExtension(new Result(fwd, NAMESPACE, queryRequest.getQueryid(), archivedMessage.getId().toString()));
		session.process(messagePacket);
	}

	/**
	 * Declare DataForm fields supported by the MAM implementation on this server
	 * @param packet Incoming query (form field request) packet
	 * @param session Session with client
	 */
	private IQ buildSupportedFieldsResult(IQ packet, Session session) {

		IQ result = IQ.createResultIQ(packet);

		Element query = result.setChildElement("query", NAMESPACE);

		DataForm form = new DataForm(DataForm.Type.form);
		form.addField("FORM_TYPE", null, FormField.Type.hidden);
		form.getField("FORM_TYPE").addValue(NAMESPACE);
		form.addField("with", null, FormField.Type.jid_single);
		form.addField("start", null, FormField.Type.text_single);
		form.addField("end", null, FormField.Type.text_single);

		query.add(form.getElement());

		return result;
	}

	@Override
	public Iterator<String> getFeatures() {
		return Collections.singleton(NAMESPACE).iterator();
	}

	void completeFinElement(QueryRequest queryRequest, Element fin) {
		if(queryRequest.getQueryid() != null) {
			fin.addAttribute("queryid", queryRequest.getQueryid());
		}

		XmppResultSet resultSet = queryRequest.getResultSet();
		if (resultSet != null) {
			fin.add(resultSet.createResultElement());

			if(resultSet.isComplete()) {
				fin.addAttribute("complete", "true");
			}
		}
	}
}
