# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import print_function
"""
This module implements the Resource classes that translate JSON from JIRA REST resources
into usable objects.
"""

import collections
import re
import logging
import json

from six import iteritems, string_types, text_type

from .utils import threaded_requests, json_loads, CaseInsensitiveDict

def get_error_list(r):
    error_list = []
    if r.status_code >= 400:
        if r.status_code == 403 and "x-authentication-denied-reason" in r.headers:
            error_list = [r.headers["x-authentication-denied-reason"]]
        elif r.text:
            try:
                response = json_loads(r)
                if 'message' in response:
                    # JIRA 5.1 errors
                    error_list = [response['message']]
                elif 'errorMessages' in response and len(response['errorMessages']) > 0:
                    # JIRA 5.0.x error messages sometimes come wrapped in this array
                    # Sometimes this is present but empty
                    errorMessages = response['errorMessages']
                    if isinstance(errorMessages, (list, tuple)):
                        error_list = errorMessages
                    else:
                        error_list = [errorMessages]
                elif 'errors' in response and len(response['errors']) > 0:
                    # JIRA 6.x error messages are found in this array.
                    error_list = response['errors'].values()
                else:
                    error_list = [r.text]
            except ValueError:
                error_list = [r.text]
    return error_list

class Resource(object):

    """
    Models a URL-addressable resource in the JIRA REST API.

    All Resource objects provide the following:
    ``find()`` -- get a resource from the server and load it into the current object
    (though clients should use the methods in the JIRA class instead of this method directly)
    ``update()`` -- changes the value of this resource on the server and returns a new resource object for it
    ``delete()`` -- deletes this resource from the server
    ``self`` -- the URL of this resource on the server
    ``raw`` -- dict of properties parsed out of the JSON response from the server

    Subclasses will implement ``update()`` and ``delete()`` as appropriate for the specific resource.

    All Resources have a resource path of the form:

    * ``issue``
    * ``project/{0}``
    * ``issue/{0}/votes``
    * ``issue/{0}/comment/{1}``

    where the bracketed numerals are placeholders for ID values that are filled in from the
    ``ids`` parameter to ``find()``.
    """

    # A prioritized list of the keys in self.raw most likely to contain a human
    # readable name or identifier, or that offer other key information.
    _READABLE_IDS = ('displayName', 'key', 'name', 'filename', 'value',
                     'scope', 'votes', 'id', 'mimeType', 'closed')

    def __init__(self, resource, options, session):
        self._resource = resource
        self._options = options
        self._session = session

        # Explicitly define as None so we know when a resource has actually
        # been loaded
        self.raw = None

    def __str__(self):
        # Return the first value we find that is likely to be human readable.
        if self.raw:
            for name in self._READABLE_IDS:
                if name in self.raw:
                    pretty_name = text_type(self.raw[name])
                    # Include any child to support nested select fields.
                    if hasattr(self, 'child'):
                        pretty_name += ' - ' + text_type(self.child)
                    return pretty_name

        # If all else fails, use repr to make sure we get something.
        return repr(self)

    def __repr__(self):
        # Identify the class and include any and all relevant values.
        names = []
        if self.raw:
            for name in self._READABLE_IDS:
                if name in self.raw:
                    names.append(name + '=' + repr(self.raw[name]))
        if not names:
            return '<JIRA %s at %s>' % (self.__class__.__name__,
                                        text_type(hex(id(self))))
        return '<JIRA %s: %s>' % (self.__class__.__name__, ', '.join(names))

    def __getattr__(self
, item):
        # this should make Project.key and similar to work
        try:

            return self[item]
        except Exception as e:
            # Make sure pickling doesn't break
            #   *MORE INFO*: This conditional wouldn't be necessary if __getattr__ wasn't used. But
            #                since it is in use (no worries), we need to give the pickle.dump*
            #                methods what they expect back. They expect to either get a KeyError
            #                exception or a tuple of args to be passed to the __new__ method upon
            #                unpickling (i.e. pickle.load* methods).
            #   *NOTE*: if the __new__ method were to be implemented in this class, this may have
            #           to be removed or changed.
            if item == '__getnewargs__':
                raise KeyError(item)

            if hasattr(self, 'raw') and item in self.raw:
                return self.raw[item]
            else:
                raise AttributeError("%r object has no attribute %r" % (self.__class__, item))
    # def __getstate__(self):
    #     """
    #     Pickling the resource; using the raw dict
    #     """
    #     return self.raw
    #
    # def __setstate__(self, raw_pickled):
    #     """
    #     Unpickling of the resource
    #     """
    #     self._parse_raw(raw_pickled)
    #

    def find(self, id, params=None):

        if params is None:
            params = {}

        url = '{server}/rest/{rest_path}/{rest_api_version}/'.format(
            **self._options)
        if isinstance(id, tuple):
            url += self._resource.format(*id)
        else:
            url += self._resource.format(id)

        self._load(url, params=params)

    def update(self, fields=None, async=None, jira=None, **kwargs):
        """
        Update this resource on the server. Keyword arguments are marshalled into a dict before being sent. If this
        resource doesn't support ``PUT``, a :py:exc:`.JIRAError` will be raised; subclasses that specialize this method
        will only raise errors in case of user error.

        :param async: if true the request will be added to the queue so it can be executed later using async_run()
        """
        if async is None:
            async = self._options['async']

        data = {}
        if fields is not None:
            data.update(fields)
        data.update(kwargs)

        data = json.dumps(data)

        r = self._session.put(
            self.self, data=data)
        if 'autofix' in self._options and \
                r.status_code == 400:
            user = None
            error_list = get_error_list(r)
            logging.error(error_list)
            if "The reporter specified is not a user." in error_list:
                if 'reporter' not in data['fields']:
                    logging.warning(
                        "autofix: setting reporter to '%s' and retrying the update." % self._options['autofix'])
                    data['fields']['reporter'] = {
                        'name': self._options['autofix']}

            if "Issues must be assigned." in error_list:
                if 'assignee' not in data['fields']:
                    logging.warning("autofix: setting assignee to '%s' for %s and retrying the update." % (
                        self._options['autofix'], self.key))
                    data['fields']['assignee'] = {
                        'name': self._options['autofix']}
                    # for some reason the above approach fails on Jira 5.2.11
                    # so we need to change the assignee before

            if "Issue type is a sub-task but parent issue key or id not specified." in error_list:
                logging.warning(
                    