import json
from time import sleep

import requests


class GPError(Exception):
    pass


class GPMessage(object):
    """Wrapper for geoprocessing messages"""

    INFORMATIVE = 1
    WARNING = 2
    ERROR = 3
    EMPTY = 4
    ABORT = 5

    def __init__(self, type, message):
        """

        :param type: message type
        :param message: message text
        """
        self.type = type
        self.message = message

    def __repr__(self):
        if self.type == self.INFORMATIVE:
            return "(Message) %s" % self.message
        else:
            return "(Error) %s" % self.message

    def __str__(self):
        return str(self.message)

    def __unicode__(self):
        return str(self.message)


class GPResult(object):
    """Wrapper for geoprocessing results"""

    def __init__(self, name, type, value):
        """
        :param name: result name
        :param type: result type
        :param value: result value
        """

        self.name = name
        self.type = type
        self.value = value


class GPTask(object):
    """
    Client interface to execute a geoprocessing task.

    Tasks can be executed asynchronously, which will then poll for status, or synchronously and will hold the connection
    open while the task is being executed.  Tasks are published as asynchronous or synchronous; you must use the
    appropriate interface for the task you are executing.
    """

    NOT_SUBMITTED = 0
    WAITING = 1
    SUBMITTED = 2
    RUNNING = 3
    SUCCEEDED = 4
    FAILED = 5
    CANCELLING = 6
    CANCELLED = 7

    def __init__(self, url, parameters={}, token=None):
        """

        :param url: url of geoprocessing tool
        :param parameters: dictionary containing input parameters for tool
        :param token: authorization token generated by server, if required
        """

        self.url = url
        self.parameters = parameters
        self.token = token
        self.output_sr = None
        self.process_sr = None
        self.return_z = False
        self.return_m = False
        self.synchronous = False

        self.status = self.NOT_SUBMITTED
        self.messages = []
        self.job_id = None

    def submit_job(self, blocking=False):
        """
        Submit the task for asynchronous processing.

        :param blocking: If True, this call will continue to poll (and block) until the job is complete.
        """

        self.synchronous = False
        data = {
            'f': "json",
            'returnZ': str(self.return_z).lower(),
            'returnM': str(self.return_m).lower()
        }
        data.update(self.parameters)
        if self.output_sr:
            data['env:outputSR'] = self.output_sr
        if self.process_sr:
            data['env:processSR'] = self.process_sr

        url = "%s/submitJob" % self.url
        cookies = {}
        if self.token:
            cookies['agstoken'] = self.token
        r = requests.post(url, data=data, cookies=cookies)
        if 200 >= r.status_code < 300:
            try:
                data = json.loads(r.text, strict=False)
            except ValueError:
                raise GPError("Server did not return a valid JSON response")
            try:
                self.job_id = data['jobId']
            except KeyError:
                raise GPError("Server response is missing 'jobId' parameter")
            return self.poll(blocking=blocking)
        else:
            raise GPError("Server returned HTTP %d" % r.status_code)

    def poll(self, blocking=False):
        """
        Poll job status.

        :param blocking: If True, this call will continue to poll (and block) until the job is complete.
        """

        url = "%s/jobs/%s?f=json" % (self.url, self.job_id)

        while True:
            cookies = {}
            if self.token:
                cookies['agstoken'] = self.token
            r = requests.get(url, cookies=cookies)
            if 200 >= r.status_code < 300:
                try:
                    data = json.loads(r.text, strict=False)
                except ValueError:
                    raise GPError("Server did not return a valid JSON response")
                try:
                    status = data['jobStatus']
                except KeyError:
                    raise GPError("Server response is missing 'jobStatus' parameter")
                if status in ESRI_JOB_STATUSES:
                    self.status = ESRI_JOB_STATUSES[status]
                else:
                    raise GPError("Unrecognized job status: %s" % status)
                self._populate_messages(data.get('messages', None))
                self._populate_results(data.get('results', None))
                if not blocking or self.status in (self.SUCCEEDED, self.FAILED, self.CANCELLED):
                    return self.status
                else:
                    sleep(1)
                    continue
            else:
                raise GPError("Server returned HTTP %d" % r.status_code)

    def execute(self):
        """Submit the task for synchronous processing."""

        self.synchronous = True
        data = {
            'f': "json",
            'returnZ': str(self.return_z).lower(),
            'returnM': str(self.return_m).lower()
        }
        data.update(self.parameters)
        if self.output_sr:
            data['env:outputSR'] = self.output_sr
        if self.process_sr:
            data['env:processSR'] = self.process_sr
        url = "%s/execute" % self.url
        cookies = {}
        if self.token:
            cookies['agstoken'] = self.token
        r = requests.post(url, data=data, cookies=cookies)
        if 200 >= r.status_code < 300:
            try:
                data = json.loads(r.text, strict=False)
            except ValueError:
                raise GPError("Server did not return a valid JSON response")
            if data.get("error", None):
                self.status = self.FAILED
                self._populate_messages(data['error'].get('details', None))
                return self.status
            self.status = self.SUCCEEDED
            self._populate_messages(data.get('messages', None))
            self._populate_results(data.get('results', None))
            return self.status
        else:
            raise GPError("Server returned HTTP %d" % r.status_code)

    def _populate_messages(self, messages):
        self.messages = []
        if isinstance(messages, list):
            for message in messages:
                if isinstance(message, dict) and 'type' in message and 'description' in message:
                    if message['type'] in ESRI_MESSAGE_TYPES:
                        type = ESRI_MESSAGE_TYPES[message['type']]
                    else:
                        continue
                    self.messages.append(GPMessage(type, message['description']))
                elif isinstance(message, str):
                    self.messages.append(GPMessage(GPMessage.ERROR, message))

    def _populate_results(self, results):
        self.results = {}
        if self.synchronous and isinstance(results, list):
            for result in results:
                if isinstance(result, dict) and 'paramName' in result and 'dataType' in result:
                    self.results[result['paramName']] = GPResult(
                        result['paramName'],
                        result['dataType'],
                        result['value']
                    )
        elif not self.synchronous and isinstance(results, dict):
            for k, v in results.items():
                cookies = {}
                if self.token:
                    cookies['agstoken'] = self.token
                r = requests.get("%s/jobs/%s/%s?f=json" % (self.url, self.job_id, v['paramUrl']), cookies=cookies)
                data = json.loads(r.text, strict=False)
                self.results[data['paramName']] = GPResult(
                    data['paramName'],
                    data['dataType'],
                    data['value']
                )


ESRI_JOB_STATUSES = {
    'esriJobWaiting': GPTask.WAITING,
    'esriJobSubmitted': GPTask.SUBMITTED,
    'esriJobExecuting': GPTask.RUNNING,
    'esriJobSucceeded': GPTask.SUCCEEDED,
    'esriJobFailed': GPTask.FAILED,
    'esriJobCancelling': GPTask.CANCELLING,
    'esriJobCancelled': GPTask.CANCELLED
}

ESRI_MESSAGE_TYPES = {
    'esriJobMessageTypeInformative': GPMessage.INFORMATIVE,
    'esriJobMessageTypeWarning': GPMessage.WARNING,
    'esriJobMessageTypeError': GPMessage.ERROR,
    'esriJobMessageTypeEmpty': GPMessage.EMPTY,
    'esriJobMessageTypeAbort': GPMessage.ABORT,
    'esriGPMessageTypeInformative': GPMessage.INFORMATIVE,
    'esriGPMessageTypeWarning': GPMessage.WARNING,
    'esriGPMessageTypeError': GPMessage.ERROR,
    'esriGPMessageTypeEmpty': GPMessage.EMPTY,
    'esriGPMessageTypeAbort': GPMessage.ABORT
}