// Copyright (c) 2011 Simon Fell
//
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included 
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
// THE SOFTWARE.
//

package com.pocketsoap.salesforce.soap;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.MappingJsonFactory;
import org.codehaus.jackson.map.ObjectMapper;

import com.pocketsoap.salesforce.rest.chatter.Comment;
import com.pocketsoap.salesforce.rest.chatter.MentionMessageSegment;
import com.pocketsoap.salesforce.rest.chatter.MessageSegment;
import com.pocketsoap.salesforce.rest.chatter.TextMessageSegment;
import com.pocketsoap.salesforce.rest.user.UserService;

/**
 * This class handles all the API calls to Salesforce/Chatter using the SOAP API.
 * This just implements the miniumum set of calls we need, not the entire API.
 * 
 * @author superfell
 */
public class ChatterClient {
	public ChatterClient(String username, String password, String loginServerUrl) throws MalformedURLException {
		this.credentials = new CredentialsInfo(username, password, loginServerUrl);
	}

	private final CredentialsInfo credentials;
	private SessionInfo session;
	private String proxyURL;
	
	/**
	 * Set a proxy (if there is one)
	 * @param proxyURL
	 */
	public void setProxyURL(String proxyURL) {
		int httpIndex = proxyURL.indexOf("//");
		if (httpIndex > -1) {
			proxyURL = proxyURL.substring(httpIndex + 2);
		}
		
		this.proxyURL = proxyURL;
	}

	public String postBuild(String recordId, String title, String resultsUrl, String testHealth, Map<String, String> suspects) throws IOException, XMLStreamException, FactoryConfigurationError {
		return postBuild(recordId, title, resultsUrl, testHealth, suspects, true);
	}
	
	public void delete(String id) throws XMLStreamException, IOException {
		establishSession();
		// SaveResult is the same as DeleteResult.
		SaveResult r = makeSoapRequest(session.instanceServerUrl,
				new DeleteRequestEntity(session.sessionId, id),
				new SaveResultParser());
		if (!r.success)
			throw new SaveResultException(r);
	}
	
	private String postBuild(String recordId, String title, String resultsUrl, String testHealth, Map<String, String> suspects, boolean retryOnInvalidSession) throws IOException, XMLStreamException, FactoryConfigurationError {
		establishSession();
		String body = testHealth == null ? title : title + "\n" + testHealth;
		if (body.length() > 1000) body = body.substring(0, 998) + "\u2026";	// ...
		

		String pid = recordId == null || recordId.length() == 0 ? session.userId : recordId;
		try {
			String postId = createSoapFeedPost(pid, title, resultsUrl, body);
			if (suspects != null && !suspects.isEmpty()) {
				postSuspectsComment(postId, suspects, true);
			}
			return postId;
		} catch (SoapFaultException ex) {
			// if we were using a cached session, then it could of expired
			// by now, so we check for INVALID_SESSION, and if we see that
			// error, we'll flush the session cache and try again.
			if (retryOnInvalidSession && ex.getFaultCode().toLowerCase().contains("session")) {
				SessionCache.get().revoke(credentials);
				return postBuild(recordId, title, resultsUrl, testHealth, suspects, false);
			}
			throw ex;
		}
	}
	

	private String postSuspectsComment(String postId,  Map<String, String> suspects, boolean retryOnInvalidSession)
			throws MalformedURLException, IOException, XMLStreamException,
			FactoryConfigurationError {
		URL instanceUrl = new URL(session.instanceServerUrl);
		URL url = new URL(instanceUrl.getProtocol(),
				instanceUrl.getHost(), instanceUrl.getPort(), 
				"/services/data/v24.0/chatter/feed-items/" + postId + "/comments");
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		
		
		try {
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setRequestProperty("Content-Type", "application/json");
			conn.setRequestProperty("Accept", "application/json");
			conn.setRequestProperty("Authorization", "OAuth " + session.sessionId); 


			final Comment comment = new Comment();
			final List<MessageSegment> messageSegments = comment.getBody().getMessageSegments();
			messageSegments.add(new TextMessageSegment("Suspects: "));
			
			Iterator<Entry<String, String>> it = suspects.entrySet().iterator();
			while (it.hasNext()) {
				Entry<String, String> ids = it.next();

				String sfdcId = ids.getValue();
				if (sfdcId != null) {
					//Convert the login to an SFDC ID
					sfdcId = UserService.getUserId(session, sfdcId);
				}
				
				if (sfdcId == null) { //not mapped
					messageSegments.add(new TextMessageSegment(ids.getKey()));
				} else {
					messageSegments.add(new MentionMessageSegment(sfdcId));
				}

				if (it.hasNext()) {
					messageSegments.add(new TextMessageSegment(", "));
				}
			}

			final OutputStream bodyStream = conn.getOutputStream();
			ObjectMapper mapper = new ObjectMapper();
			MappingJsonFactory jsonFactory = new MappingJsonFactory();
			JsonGenerator jsonGenerator = jsonFactory.createJsonGenerator(bodyStream);
			mapper.writeValue(jsonGenerator, comment);
			bodyStream.close();
			conn.getInputStream().close(); //Don't care about the response
		} catch (IOException e) {
			if (retryOnInvalidSession && conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
				SessionCache.get().revoke(credentials);
				return postSuspectsComment(postId, suspects, false);
			}
			
			BufferedReader r = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
			String error = "";
			String line ;
			while ((line = r.readLine()) != null) {
				error += line + '\n';
			}
			System.out.println(error);
			throw e;
		}
		return null;
	}
	
	void establishSession() throws IOException, XMLStreamException, FactoryConfigurationError {
		SessionInfo s = SessionCache.get().getCachedSessionInfo(credentials);
		if (s == null)  s = performLogin();
		session = s;
	}
	
	private static final String SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/";
	private static final String SF_NS = "urn:partner.soap.sforce.com";
	
	public SessionInfo performLogin() throws IOException, XMLStreamException, FactoryConfigurationError {
		SessionInfo si = makeSoapRequest(new URL(this.credentials.getLoginServerUrl(), "/services/Soap/u/21.0"), new LoginRequestEntity(credentials), new LoginResponseParser());
		SessionCache.get().add(credentials, si);
		return si;
	}
	
	String createSoapFeedPost(String recordId, String title, String url, String testHealth) throws XMLStreamException, IOException {
		SaveResult sr = makeSoapRequest(session.instanceServerUrl, new FeedPostEntity(session.sessionId, recordId, title, url, testHealth), new SaveResultParser());
		if (sr.success) return sr.id;
		throw new SaveResultException(sr);
	}
	
	private static abstract class ResponseParser<T> {
		// parse a response, the reader has already been moved through the soap gubins, 
		// and is at the first child of body.
		abstract T parse(XMLStreamReader r) throws XMLStreamException;
	}
	
	private static class SaveResultParser extends ResponseParser<SaveResult> {

		@Override
		SaveResult parse(XMLStreamReader rdr) throws XMLStreamException {
			String id = null, sc = null, msg = null;
			boolean success = false;
			rdr.require(XMLStreamReader.START_ELEMENT, SF_NS, null);
			if (!rdr.getLocalName().endsWith("Response"))
				throw new XMLStreamException("expected element named *Response but was " + rdr.getLocalName(), rdr.getLocation());
			rdr.nextTag();
			rdr.require(XMLStreamReader.START_ELEMENT, SF_NS, "result");
			while (rdr.next() != XMLStreamReader.END_DOCUMENT) {
				if (rdr.getEventType() == XMLStreamReader.START_ELEMENT) {
					String ln = rdr.getLocalName();
					if (ln.equals("id")) 
						id = rdr.getElementText();
					else if (ln.equals("statusCode"))
						sc = rdr.getElementText();
					else if (ln.equals("message")) 
						msg = rdr.getElementText();
					else if (ln.equals("success")) {
						String v = rdr.getElementText();
						success = v.equals("1") || v.equalsIgnoreCase("true");
					}
				}
			}
			return new SaveResult(success, id, sc, msg);
		}
	}
	
	private static class LoginResponseParser extends ResponseParser<SessionInfo> {

		@Override
		SessionInfo parse(XMLStreamReader rdr) throws XMLStreamException {
			String sid = null, instanceUrl = null, userId = null;
			rdr.require(XMLStreamReader.START_ELEMENT, SF_NS, "loginResponse");
			while (rdr.next() != XMLStreamReader.END_DOCUMENT) {
				if (rdr.getEventType() == XMLStreamReader.START_ELEMENT) {
					String ln = rdr.getLocalName();
					if (ln.equals("sessionId")) 
						sid = rdr.getElementText();
					else if (ln.equals("serverUrl")) 
						instanceUrl = rdr.getElementText();
					else if (ln.equals("userId"))
						userId = rdr.getElementText();
				}
			}
			return new SessionInfo(sid, instanceUrl, userId);
		}
	}

	private <T> T makeSoapRequest(URL serverUrl, RequestEntity req, ResponseParser<T> respParser) throws XMLStreamException, IOException {
		return makeSoapRequest(serverUrl.toString(), req, respParser);
	}
	
	private <T> T makeSoapRequest(String serverUrl, RequestEntity req, ResponseParser<T> respParser) throws XMLStreamException, IOException {
		PostMethod post = new PostMethod(serverUrl);
		post.addRequestHeader("SOAPAction", "\"\"");
		post.setRequestEntity(req);

		HttpClient http = new HttpClient();
		
		//Set the proxy
		if (proxyURL != null && proxyURL.length() > 0) {
			String[] urlPort = proxyURL.split(":");
			if (urlPort.length == 2) {
				HostConfiguration config = new HostConfiguration();
				config.setProxy(urlPort[0], Integer.valueOf(urlPort[1]));
				http.setHostConfiguration(config);
			}
		}
		
		int sc = http.executeMethod(post);
		if (sc != 200 && sc != 500)
			throw new IOException("request to " + serverUrl + " returned unexpected HTTP status code of " + sc + ", check configuration.");
		
		XMLInputFactory f = XMLInputFactory.newInstance();
		f.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);

		XMLStreamReader rdr = f.createXMLStreamReader(post.getResponseBodyAsStream());
		rdr.require(XMLStreamReader.START_DOCUMENT, null, null);
		rdr.nextTag();
		rdr.require(XMLStreamReader.START_ELEMENT, SOAP_NS, "Envelope");
		rdr.nextTag();
		// TODO, should handle a Header appearing in the response.
		rdr.require(XMLStreamReader.START_ELEMENT, SOAP_NS, "Body");
		rdr.nextTag();
		if (rdr.getLocalName().equals("Fault")) {
			throw handleSoapFault(rdr);
		}
		try {
			T response = respParser.parse(rdr);
			while (rdr.hasNext()) rdr.next();
			return response;
		} finally {
			try {
				rdr.close();
			} finally {
				post.releaseConnection();
			}
		}
	}
	
	// read a soap fault message and turn it into an exception
	private RuntimeException handleSoapFault(XMLStreamReader rdr) throws XMLStreamException {
		String fc = null, fs = null;
		while (rdr.next() != XMLStreamReader.END_DOCUMENT) {
			if (rdr.getEventType() == XMLStreamReader.START_ELEMENT) {
				String ln = rdr.getLocalName();
				if (ln.equals("faultcode")) 
					fc = rdr.getElementText();
				else if (ln.equals("faultstring"))
					fs = rdr.getElementText();
			}
		}
		return new SoapFaultException(fc, fs);
	}
}
