package gov.va.vinci.dart.common;

import gov.va.vinci.common.RequestBase;
import gov.va.vinci.common.User;
import gov.va.vinci.dart.common.exception.CheckedException;
import gov.va.vinci.dart.common.exception.ValidationException;

import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.util.JAXBResult;
import javax.xml.bind.util.JAXBSource;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ws.client.core.WebServiceTemplate;

/**
 * Common parent class of Web Services client classes
 * 
 */
@SuppressWarnings("serial")
public abstract class ServiceClient implements Serializable {
	private static Log log = LogFactory.getLog(ServiceClient.class);
			
	private static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
	public static final SimpleDateFormat SDF_DATE = new SimpleDateFormat(
			"MM/dd/yyyy");

	// TODO- you could create this with a defined jaxb marshaller
	private final WebServiceTemplate webServiceTemplate;
	protected String endpointUrl;
	protected DatatypeFactory datatypeFactory;
	private MessageIdGenerator messageIdGenerator;
	protected String jaxbContext;
	protected String applicationId = "";
	protected User user;

	/**
	 * Create a new instance of a ServiceClient
	 * 
	 * @param jaxbContext
	 *            The JAXB context for Web Services transactions
	 * 
	 * @throws DatatypeConfigurationException
	 */
	protected ServiceClient(final String jaxbContext)
			throws DatatypeConfigurationException {
		webServiceTemplate = new WebServiceTemplate();
		messageIdGenerator = new MessageIdGenerator(this);
		datatypeFactory = DatatypeFactory.newInstance();
		this.jaxbContext = jaxbContext;

		// TODO- set the connection timeout properties on the
		// WebServiceTemplate message sender
	}

	/**
	 * Initialize required properties of a Web Services request header.
	 * 
	 * @param request
	 *            The request to initialize
	 * @return The initialized result.
	 * 
	 * @throws ValidationException
	 */
	protected RequestBase setRequestHeader(final RequestBase request)
			throws ValidationException {

		if (user == null) {
			throw new ValidationException(
					"User is required to set request header.  Call ServiceClient.setUser().");
		}

		request.setRequestMessageID(messageIdGenerator.nextMessageId());
		request.setMessageSentDateTime(toXMLGregorianCalendar((java.sql.Timestamp) null));
		request.setSendingApplication(applicationId);
		request.setUser(user);
		return request;
	}

	/**
	 * Perform a Web Services transaction with a remote server.
	 * 
	 * @param endpointUrl
	 *            The URL of the remote service.
	 * @param request
	 *            The request object to send.
	 * @return The received result object on success.
	 * @throws JAXBException
	 */
	protected Object transact(final String endpointUrl, final Object request)
			throws JAXBException {
		JAXBResult result = null;

		try {
			// DEBUGGING
			log.debug("Web Service call start: "
					+ System.currentTimeMillis() + " ms");

			JAXBSource jaxbSource = new JAXBSource(
					JAXBContext.newInstance(jaxbContext), request);

			// TODO- we could also use the unmarshaller configured in
			// applicationContext.xml
			result = new JAXBResult(JAXBContext.newInstance(jaxbContext));

			webServiceTemplate.sendSourceAndReceiveToResult(endpointUrl,
					jaxbSource, result);

			// DEBUGGING
			log.debug("Web Service call done: "
					+ System.currentTimeMillis() + " ms");
		} catch (JAXBException e) {
			// DEBUGGING
			log.debug("Web Service call exception: "
					+ System.currentTimeMillis() + " ms");
			throw e;
		}

		return result.getResult();
	}

	/**
	 * Set the default Web Services remote service URL for subsequent calls to
	 * transact()
	 * 
	 * @param uri
	 *            The URI of the remove service.
	 */
	public void setDefaultUri(final String uri) {
		webServiceTemplate.setDefaultUri(uri);
	}

	/**
	 * Set the Web Services remote service URL for subsequent calls to
	 * transact().
	 * 
	 * @param url
	 *            The URL of the remove service.
	 */
	public void setEndpointUrl(final String url) {
		this.endpointUrl = url;
	}

	/**
	 * Convert a Timestamp object to an XMLGregorianCalendar for marshalling as
	 * part of a Web Services request.
	 * 
	 * @param timestamp
	 *            The timestamp object to convert.
	 * @return The resulting XMLGregorianCalendar object. If the passed
	 *         timestamp is null, the result will contain the current system
	 *         time.
	 */
	protected XMLGregorianCalendar toXMLGregorianCalendar(
			final java.sql.Timestamp timestamp) {

		// Note: assuming the same timezone in the date as the current system.
		GregorianCalendar gc = new GregorianCalendar();
		if (timestamp != null) {
			gc.setTimeInMillis(timestamp.getTime());
		} else {
			gc.setTimeInMillis(System.currentTimeMillis());
		}
		gc.setTimeZone(GMT_TIMEZONE);
		
		return datatypeFactory.newXMLGregorianCalendar(gc);
	}

	/**
	 * Parse a String object containing a date in mm/dd/yyyy format and convert
	 * the result to an XMLGregorianCalendar for marshalling as part of a Web
	 * Services request.
	 * 
	 * @param dateStr
	 *            The date string to parse.
	 * @return The resulting XMLGregorianCalendar object. If the passed
	 *         timestamp is null, the result will contain the current system
	 *         time.
	 * @throws CheckedException
	 *             On parse error.
	 */
	protected XMLGregorianCalendar toXMLGregorianCalendar(final String dateStr)
			throws CheckedException {
		java.util.Date date;

		try {
			date = SDF_DATE.parse(dateStr);
		} catch (ParseException e) {
			throw new CheckedException("Error parsing date", e);
		}

		GregorianCalendar gc = new GregorianCalendar();

		if (dateStr != null) {
			gc.setTimeInMillis(date.getTime());
		} else {
			gc.setTimeInMillis(System.currentTimeMillis());
		}
		gc.setTimeZone(GMT_TIMEZONE);
		
		return datatypeFactory.newXMLGregorianCalendar(gc);
	}

	/**
	 * Convert a Date object to an XMLGregorianCalendar for marshalling as part
	 * of a Web Services request.
	 * 
	 * @param date
	 *            The date object to convert.
	 * @return The resulting XMLGregorianCalendar object. If the passed date is
	 *         null, the result will contain the current system time.
	 */
	protected XMLGregorianCalendar toXMLGregorianCalendar(
			final java.util.Date date) {

		// Note: assuming the same timezone in the date as the current system.
		GregorianCalendar gc = new GregorianCalendar();
		if (date != null) {
			gc.setTimeInMillis(date.getTime());
		} else {
			gc.setTimeInMillis(System.currentTimeMillis());
		}
		gc.setTimeZone(GMT_TIMEZONE);

		return datatypeFactory.newXMLGregorianCalendar(gc);
	}

	public String getApplicationId() {
		return applicationId;
	}

	/**
	 * Set the application id used for credentials in the remote Web Services
	 * server.
	 * 
	 * @param applicationId
	 *            The application id to pass to the remote server.
	 */
	protected void setApplicationId(final String applicationId) {
		this.applicationId = applicationId;
	}

	public User getUser() {
		return user;
	}

	protected void setUser(final User user) {
		this.user = user;
	}

	/**
	 * Set the user id used for credentials in the remote Web Services server.
	 * 
	 * @param username
	 *            The user name to pass to the remote server.
	 */
	public void setUserId(final String username) {
		// TODO- get a better user credentials implementation, please.
		User user = new User();
		user.setUsername(username);
		setUser(user);
	}
}
