import copy
import getpass
import logging
import pprint
import urllib
import urlparse

import httplib2
from autotest.client.shared import utils
from autotest.frontend.afe import rpc_client_lib
from django.utils import datastructures, simplejson

_request_headers = {}


def _get_request_headers(uri):
    # ParseResult is subscriptable, ignore E1136
    server = urlparse.urlparse(uri)[0:2]    # pylint: disable=E1136
    if server in _request_headers:
        return _request_headers[server]

    headers = rpc_client_lib.authorization_headers(getpass.getuser(), uri)
    headers['Content-Type'] = 'application/json'

    _request_headers[server] = headers
    return headers


def _clear_request_headers(uri):
    # ParseResult is subscriptable, ignore E1136
    server = urlparse.urlparse(uri)[0:2]    # pylint: disable=E1136
    if server in _request_headers:
        del _request_headers[server]


def _site_verify_response_default(headers, response_body):
    return headers['status'] != '401'


class RestClientError(Exception):
    pass


class ClientError(Exception):
    pass


class ServerError(Exception):
    pass


class Response(object):

    def __init__(self, httplib_response, httplib_content):
        self.status = int(httplib_response['status'])
        self.headers = httplib_response
        self.entity_body = httplib_content

    def decoded_body(self):
        return simplejson.loads(self.entity_body)

    def __str__(self):
        return '\n'.join([str(self.status), self.entity_body])


class Resource(object):

    def __init__(self, representation_dict, http):
        self._http = http
        assert 'href' in representation_dict
        for key, value in representation_dict.iteritems():
            setattr(self, str(key), value)

    def __repr__(self):
        return 'Resource(%r)' % self._representation()

    def pprint(self):
        # pretty-print support for debugging/interactive use
        pprint.pprint(self._representation())

    @classmethod
    def load(cls, uri, http=None):
        if not http:
            http = httplib2.Http()
        directory = cls({'href': uri}, http)
        return directory.get()

    def _read_representation(self, value):
        # recursively convert representation dicts to Resource objects
        if isinstance(value, list):
            return [self._read_representation(element) for element in value]
        if isinstance(value, dict):
            converted_dict = dict((key, self._read_representation(sub_value))
                                  for key, sub_value in value.iteritems())
            if 'href' in converted_dict:
                return type(self)(converted_dict, http=self._http)
            return converted_dict
        return value

    def _write_representation(self, value):
        # recursively convert Resource objects to representation dicts
        if isinstance(value, list):
            return [self._write_representation(element) for element in value]
        if isinstance(value, dict):
            return dict((key, self._write_representation(sub_value))
                        for key, sub_value in value.iteritems())
        if isinstance(value, Resource):
            return value._representation()
        return value

    def _representation(self):
        return dict((key, self._write_representation(value))
                    for key, value in self.__dict__.iteritems() if not
                    key.startswith('_') and not callable(value))

    def _do_request(self, method, uri, query_parameters, encoded_body):
        uri_parts = [uri]
        if query_parameters:
            if '?' in uri:
                uri_parts += '&'
            else:
                uri_parts += '?'
            uri_parts += urllib.urlencode(query_parameters, doseq=True)
        full_uri = ''.join(uri_parts)

        if encoded_body:
            entity_body = simplejson.dumps(encoded_body)
        else:
            entity_body = None

        logging.debug('%s %s', method, full_uri)
        if entity_body:
            logging.debug(entity_body)

        site_verify = utils.import_site_function(
            __file__, 'autotest.frontend.shared.site_rest_client',
            'site_verify_response', _site_verify_response_default)
        headers, response_body = self._http.request(
            full_uri, method, body=entity_body,
            headers=_get_request_headers(uri))
        if not site_verify(headers, response_body):
            logging.debug('Response verification failed, clearing headers and '
                          'trying again:\n%s', response_body)
            _clear_request_headers(uri)
            headers, response_body = self._http.request(
                full_uri, method, body=entity_body,
                headers=_get_request_headers(uri))

        logging.debug('Response: %s', headers['status'])

        return Response(headers, response_body)

    def _request(self, method, query_parameters=None, encoded_body=None):
        if query_parameters is None:
            query_parameters = {}

        response = self._do_request(method, self.href, query_parameters,
                                    encoded_body)

        if 300 <= response.status < 400:  # redirection
            return self._do_request(method, response.headers['location'],
                                    query_parameters, encoded_body)
        if 400 <= response.status < 500:
            raise ClientError(str(response))
        if 500 <= response.status < 600:
            raise ServerError(str(response))
        return response

    def _stringify_query_parameter(self, value):
        if isinstance(value, (list, tuple)):
            return ','.join(self._stringify_query_parameter(item)
                            for item in value)
        return str(value)

    def _iterlists(self, mapping):
        """This effectively lets us treat dicts as MultiValueDicts."""
        if hasattr(mapping, 'iterlists'):  # mapping is already a MultiValueDict
            return mapping.iterlists()
        return ((key, (value,)) for key, value in mapping.iteritems())

    def get(self, query_parameters=None, **kwarg_query_parameters):
        """
        :param query_parameters: a dict or MultiValueDict
        """
        query_parameters = copy.copy(query_parameters)  # avoid mutating original
        if query_parameters is None:
            query_parameters = {}
        query_parameters.update(kwarg_query_parameters)

        string_parameters = datastructures.MultiValueDict()
        for key, values in self._iterlists(query_parameters):
            string_parameters.setlist(
                key, [self._stringify_query_parameter(value)
                      for value in values])

        response = self._request('GET',
                                 query_parameters=string_parameters.lists())
        assert response.status == 200
        return self._read_representation(response.decoded_body())

    def get_full(self, results_limit, query_parameters=None,
                 **kwarg_query_parameters):
        """
        Like get() for collections, when the full collection is expected.

        :param results_limit: maxmimum number of results to allow
        :raise ClientError if there are more than results_limit results.
        """
        result = self.get(query_parameters=query_parameters,
                          items_per_page=results_limit,
                          **kwarg_query_parameters)
        if result.total_results > results_limit:
            raise ClientError(
                'Too many results (%s > %s) for request %s (%s %s)'
                % (result.total_results, results_limit, self.href,
                   query_parameters, kwarg_query_parameters))
        return result

    def put(self):
        response = self._request('PUT', encoded_body=self._representation())
        assert response.status == 200
        return self._read_representation(response.decoded_body())

    def delete(self):
        response = self._request('DELETE')
        assert response.status == 204  # no content

    def post(self, request_dict):
        # request_dict may still have resources in it
        request_dict = self._write_representation(request_dict)
        response = self._request('POST', encoded_body=request_dict)
        assert response.status == 201  # created
        return self._read_representation({'href': response.headers['location']})
