#! /usr/bin/env python3
# coding=UTF-8
# Client for launching simulations for the Besancon Galaxy Model web service and Gravpot web service.
# Version 1.0,
# Date: 2021 january 6th.
# Author: Raphael Melior
# Institut UTINAM, OSU THETA, France
#
# .Change log:
#   - 2021-01-06:
#        model URL to model https://model.obs-besancon.fr/ws/ in how to
#        use "python3" in shebang
#   - 2019-01-19:
#        model URL turned from http to https
#   - 2019-01-21
#        compatible python3 (add parentesis of print functions, long() => int())

# http://docs.python-requests.org/en/latest/user/quickstart/
# https://docs.python.org/2/library/xml.etree.elementtree.html

#________________________
#Example of usage for Besancon Galaxy Model web service:
#   How to launch a simulation :
#./galmod_client.py --url "https://model.obs-besancon.fr/ws/" --user username --create -p Coor1_min 90. -p Coor1_max 91. -p Coor2_min 30. -p Coor2_max 31. -p SOLI 1 -p ref_filter V -p band_max 20.,99.,99.,99.,99.,99.,99.,99.,99. --run
#
#   To see the status of a job :
#./galmod_client.py --url "https://model.obs-besancon.fr/ws/" --user username --job jobnumber
#
#   To see options and actions :
#./galmod_client.py --help
#________________________

import argparse
import datetime
import dateutil.parser
import requests # installs with : pip install requests
import xml.etree.ElementTree as ElementTree
import os
import getpass


XML_NS = {'uws':'http://www.ivoa.net/xml/UWS/v1.0', 'xlink':'http://www.w3.org/1999/xlink'}

class UWS:

	def __init__(self, url, auth):
		self._url = url
		self._auth = auth

	def getJobsList(self):
		r = requests.get(self._url+"/jobs", auth=self._auth)
		return self._getJobInfosFromReqResult(r, "get list of jobs")

	def createJob(self):
		r = requests.post(self._url+"/jobs", auth=self._auth)
		return self._getJobInfosFromReqResult(r, "creating job")

	def getJobInfos(self, idJob):
		r = requests.get(self._url+"/jobs/"+str(idJob), auth=self._auth)
		return self._getJobInfosFromReqResult(r, "get details of a job")

	def deleteJob(self, idJob):
		r = requests.delete(self._url+"/jobs/"+str(idJob), auth=self._auth)
		return self._getJobInfosFromReqResult(r, "deleting a job")

	def runJob(self, idJob):
		r = requests.post(self._url+"/jobs/"+str(idJob)+'/phase', auth=self._auth, data={'PHASE':'RUN'})
		return self._getJobInfosFromReqResult(r, "starting a job")

	def abortJob(self, idJob):
		r = requests.post(self._url+"/jobs/"+str(idJob)+'/phase', auth=self._auth, data={'PHASE':'ABORT'})
		return self._getJobInfosFromReqResult(r, "aborting a job")

	def maxDurationJob(self, idJob, seconds):
		r = requests.post(self._url+"/jobs/"+str(idJob)+'/executionduration', auth=self._auth, data={'EXECUTIONDURATION':seconds})
		return self._getJobInfosFromReqResult(r, "setting execution duration of a job")

	def timeDestructJob(self, idJob, timeStr):
		r = requests.post(self._url+"/jobs/"+str(idJob)+'/destruction', auth=self._auth, data={'DESTRUCTION':timeStr})
		return self._getJobInfosFromReqResult(r, "setting execution duration of a job")

	def setParams(self, idJob, params):
		#if len(params) == 1:	# TODO
		#	r = requests.put(self._url+"/jobs/"+str(idJob)+'/parameters/'+params[0][0], data=params[0][1])
		#	print(r.text)
		#	return getJobInfos(idJob)#self._getJobInfosFromReqResult(r, "setting parameters of a job")
		#elif len(params) > 1:
			data={}
			for p in params:
				data[p[0]] = p[1]
			r = requests.post(self._url+"/jobs/"+str(idJob)+'/parameters', auth=self._auth, data=data)
			return self._getJobInfosFromReqResult(r, "setting parameters of a job")

	def _getJobInfosFromReqResult(self, r, phase):
		if (len(r.history) > 0):
			print("SERVER RESPONSE : "+r.history[0].text)

		if (r.status_code == 200):
			#et = ElementTree
			#ElementTree.register_namespace('uws', 'http://www.ivoa.net/xml/UWS/v1.0')
			for ns in XML_NS:
				ElementTree.register_namespace(ns, XML_NS[ns])

			try:
				return ElementTree.fromstring(r.text)
			except Exception as e:
				print("Invalid response :")
				print(r.text)
				raise e
		else:
			raise JobError(r, phase)




# Convertion into text

def xmlTextIfFound(xmlElem):
	if (xmlElem == None) or (xmlElem.text == None):
		return ''
	else:
		return xmlElem.text

def xmlDateIfFound(xmlElem):
	if (xmlElem == None) or (xmlElem.text == None):
		return '-'
	else:
		d = dateutil.parser.parse(xmlElem.text).astimezone(dateutil.tz.tzutc())
		return d.strftime("%Y-%m-%d %H:%M")


# Exceptions

class JobError(Exception):
	def __init__(self, http, when):
		self.status = http.status_code
		self.explain = http.text
		self.when = when
	def __str__(self):
		return "Error when "+self.when+" : "+str(self.status)+" "+self.explain



# Main

if __name__=="__main__":
	parser = argparse.ArgumentParser(description='Makes requests to web services, either the Besancon Model of the Galaxy or Gravpot web service', add_help=False)

	serverGrp = parser.add_argument_group('Server options', 'Options for server connection.')
	serverGrp.add_argument('--url', help='URL of the server.', default='https://model.obs-besancon.fr/ws')
	serverGrp.add_argument('--user', help='Username used to connect to server, by default it is the system username ('+os.getlogin()+').', default=os.getlogin())
	serverGrp.add_argument('--pass', dest='passwd', help='Password used to connect to server, by default the password is prompted. Usage of this option is discouraged because it will be visible, for example, with "ps" command and written to history.')

	jobGrp = parser.add_argument_group('Job selection', 'Select the job you want to manipulate. To create a new one type "--create". If this option is not present, it returns your list of jobs and exit ignoring options in "Actions".').add_mutually_exclusive_group()
	jobGrp.add_argument('-c', '--create', help='Create a new job.', action='store_true')
	jobGrp.add_argument('-j', '--job', type=int, help='The job affected by this request.')

	actionGrp = parser.add_argument_group('Actions', 'Configure and make actions on the selected job.')
	actionGrp.add_argument('-p', '--param', help='Set parameter named PARAMNAME with VALUE.', action='append', nargs=2, metavar=('PARAMNAME', 'VALUE'))
	actionGrp.add_argument('--execdur', help='Set maximum execution duration of a job in seconds (this setting can be overridden by server\'s configuration).', type=int)
	actionGrp.add_argument('--tdest', help='Set destruction time of a job (date+time in ISO8601 format, this setting can be overridden by server\'s configuration).')
	actionGrp.add_argument('--run', help='Send job for computation.', action='store_true')
	actionGrp.add_argument('--abort', help='Abort the job.', action='store_true')
	actionGrp.add_argument('--delete', help='Delete the job.', action='store_true')

	miscGrp = parser.add_argument_group('Miscellaneous')
	miscGrp.add_argument('-h', '--help', help='Show this help message and exit.', action='store_true')

	args = parser.parse_args()

	if (args.help):
		parser.print_help()
		exit(0)

	if (args.passwd != None):
		passwd = args.passwd
	else:
		passwd = getpass.getpass('Password for '+args.user+': ')

	#print(args.user+' '+passwd)
	uws = UWS(args.url, (args.user, passwd))

	#print(args) #args.output
	job = None
	if (args.create):
		try:
			print("Creating job")
			job = uws.createJob()
		except JobError as e:
			print(e)
			exit(2)

	elif (args.job != None):
		try:
			job = uws.getJobInfos(args.job)
		except JobError as e:
			print(e)
			exit(2)

	else:
		print(" JobID        phase       startTime          endTime            destruction    ")
		print("----------   ---------   ----------------   ----------------   ----------------")
		#print("0123456789   COMPLETED   2009-05-19 17:12   2009-05-19 17:15   2009-06-06 17:12")
		jobs = uws.getJobsList()
		for job in jobs.findall('uws:job', XML_NS):
			jobId = xmlTextIfFound(job.find('uws:jobId', XML_NS))
			phase = xmlTextIfFound(job.find('uws:phase', XML_NS))
			startTime = xmlDateIfFound(job.find('uws:startTime', XML_NS))
			endTime = xmlDateIfFound(job.find('uws:endTime', XML_NS))
			destruction = xmlDateIfFound(job.find('uws:destruction', XML_NS))
			print("%10d   %9s   %16s   %16s   %16s" % (int(jobId), phase, startTime, endTime, destruction))
		exit(0)


	idJob = int(job.find('uws:jobId', XML_NS).text)

	if args.param:
		job = uws.setParams(idJob, args.param)

	if args.execdur:
		job = uws.maxDurationJob(idJob, args.execdur)

	if args.tdest:
		job = uws.timeDestructJob(idJob, args.tdest)

	if args.run:
		job = uws.runJob(idJob)
	elif args.abort:
		job = uws.abortJob(idJob)

	print("id                :"+str(idJob))
	print("owner             :"+xmlTextIfFound(job.find('uws:ownerId', XML_NS)))
	print("phase             :"+xmlTextIfFound(job.find('uws:phase', XML_NS)))
	print("startTime         :"+xmlTextIfFound(job.find('uws:startTime', XML_NS)))
	print("endTime           :"+xmlTextIfFound(job.find('uws:endTime', XML_NS)))
	print("destruction       :"+xmlTextIfFound(job.find('uws:destruction', XML_NS)))
	print("executionDuration :"+xmlTextIfFound(job.find('uws:executionDuration', XML_NS)))

	parameters = job.find('uws:parameters', XML_NS)
	if parameters is not None:
		print('\nParameters ........')
		for param in parameters.findall('uws:parameter', XML_NS):
			print('{:<18}:{}'.format(param.get('id'), xmlTextIfFound(param)))

	errorSummary = job.find('uws:errorSummary', XML_NS)
	if errorSummary is not None:
		print('\nErrors ............')
		for err in errorSummary.findall('uws:message', XML_NS):
			print('error             :'+xmlTextIfFound(err))

	results = job.find('uws:results', XML_NS)
	if results is not None:
		print('\nResults ...........')
		for res in results.findall('uws:result', XML_NS):
			print('{:<18}:{}'.format(res.get('id'), res.get('{http://www.w3.org/1999/xlink}href')))

	if args.delete:
		uws.deleteJob(idJob)

