from __future__ import absolute_import
from requests.compat import urlencode, urldefrag
import socket
from hashlib import sha1
import requests
from requests.structures import CaseInsensitiveDict
import json
import types
import sys
from dragline import runtime

try:
    from requests.packages import urllib3
    urllib3.disable_warnings()
except:
    pass
socket.setdefaulttimeout(300)


class RequestError(Exception):
    pass


class Request(object):

    """
    :param url: URL to send.
    :param method: HTTP method to use.
    :param callback: name of the function to call after url is downloaded.
    :param meta:  A dict that contains arbitrary metadata for this request.
    :param headers: dictionary of headers to send.
    :param files: dictionary of {filename: fileobject} files to multipart upload.
    :param data: the body to attach to the request. If a dictionary is provided, form-encoding will take place.
    :param json: json for the body to attach to the request (if data is not specified).
    :param params: dictionary of URL parameters to append to the URL.
    :param auth: Auth handler or (user, pass) tuple.
    :param cookies: dictionary or CookieJar of cookies to attach to this request.
    :param timeout: (optional) How long to wait for the server to send
        data before giving up, as a float, or a tuple.
    :type timeout: float or tuple
    :param allow_redirects: (optional) Set to True by default.
    :type allow_redirects: bool
    :param proxies: (optional) Dictionary mapping protocol to the URL of
        the proxy.
    :param stream: (optional) whether to immediately download the response
        content. Defaults to ``False``.
    :param verify: (optional) if ``True``, the SSL cert will be verified.
        A CA_BUNDLE path can also be provided.
    :param cert: (optional) if String, path to ssl client cert file (.pem).
        If Tuple, ('cert', 'key') pair.
    """
    callback = None
    meta = None
    dont_filter = False
    retry = 0

    def __init__(self, url, method=None, callback=None, meta=None, headers=None, files=None, data=None, json=None,
                 params=None, auth=None, cookies=None, timeout=None, allow_redirects=None, proxies=None, stream=None,
                 verify=None, cert=None, dont_filter=None):
        args = dict(url=url, method=method, headers=headers, files=files, data=data, json=json, params=params,
                    auth=auth, cookies=cookies, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies,
                    stream=stream, verify=verify, cert=cert)
        self.request_args = {k: v for k, v in args.items() if v is not None}
        if callback is not None:
            self.callback = callback
        if meta is not None:
            self.meta = meta
        if dont_filter:
            self.dont_filter = True

    def __getstate__(self):
        d = self.__dict__.copy()
        if isinstance(self.callback, types.MethodType) and hasattr(self.callback, 'im_self'):
            d['callback'] = self.callback.__name__
            if not runtime.spider == self.callback.im_self:
                runtime.spider = self.callback.im_self
        return d

    def __setstate__(self, d):
        if 'callback' in d and isinstance(d['callback'], str):
            d['callback'] = getattr(runtime.spider, d['callback'])
        self.__dict__ = d

    def __repr__(self):
        return "<%s>" % self.get_unique_id()

    def __str__(self):
        return self.get_unique_id()

    def __usha1(self, x):
        """sha1 with unicode support"""
        if isinstance(x, unicode):
            return sha1(x.encode('utf-8')).hexdigest()
        else:
            return sha1(x).hexdigest()

    def __getattribute__(self, name):
        try:
            return object.__getattribute__(self, name)
        except AttributeError as e:
            return self.request_args[name]
        raise e

    def get_args(self, keys=None):
        args = runtime.settings.DEFAULT_REQUEST_ARGS.copy()
        args.update(self.request_args)
        if args['method'] is None:
            if any((args['data'], args['json'])):
                args['method'] = "POST"
            else:
                args['method'] = "GET"
        # if 'timeout' not in args:
        #    args['timeout'] = max(self.settings.DELAY, self.settings.TIMEOUT)
        for i in args:
            if callable(args[i]):
                args[i] = args[i]()
        if keys:
            return {k: args[k] for k in keys}
        return args

    def send(self):
        """
        This function sends HTTP requests.

        :returns: response
        :rtype: :class:`dragline.http.Response`
        :raises: :exc:`dragline.http.RequestError`: when failed to fetch contents

        >>> req = Request("http://www.example.org")
        >>> response = req.send()
        >>> print response.headers['status']
        200

        """
        try:
            response = runtime.request_processor.get_response(**self.get_args())
            response.meta = self.meta
            return response
        except KeyboardInterrupt:
            raise KeyboardInterrupt
        except:
            exc_class, exc, tb = sys.exc_info()
            new_exc = RequestError("Failed to send Request on %s: %s" % (self, exc))
            raise new_exc.__class__, new_exc, tb

    def get_unique_id(self, hashing=False):
        keys = ["method", "url", "data", "params", "json"]
        args = self.get_args(keys)
        args["url"] = urldefrag(args["url"])[0]
        request = ":".join(args[k] for k in keys[:2])
        if args["params"]:
            request += "?" + self._encode_params(args["params"])
        body = None
        if args["json"]:
            body = json.dumps(args["json"])
        elif args["data"]:
            body = self._encode_params(args["data"])
        if body:
            request += ":" + body
        if hashing:
            return self.__usha1(request)
        else:
            return request

    @staticmethod
    def _encode_params(data):
        """Encode parameters in a piece of data.

        Will successfully encode parameters when passed as a dict or a list of
        2-tuples. Order is retained if data is a list of 2-tuples but arbitrary
        if parameters are supplied as a dict.
        """
        if data is None:
            return ''
        elif isinstance(data, basestring):
            return data
        elif hasattr(data, 'read'):
            return data
        elif hasattr(data, '__iter__'):
            result = []
            for k, vs in sorted(CaseInsensitiveDict(data).lower_items()):
                if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):
                    vs = [vs]
                for v in vs:
                    if v is not None:
                        result.append(
                            (k.encode('utf-8') if isinstance(k, str) else k,
                             v.encode('utf-8') if isinstance(v, str) else v))
            return urlencode(result, doseq=True)
        else:
            return data


class RequestProcessor(object):
    def get_response(self, **kwargs):
        session = requests.Session()
        response = session.request(**kwargs)

        if response.encoding == 'ISO-8859-1' and 'ISO-8859-1' not in response.headers.get('Content-Type', ''):
                response.encoding = response.apparent_encoding
        response.cookies = session.cookies
        return Response(response)

    def put_response(self, response):
        del response

    def clear(self):
        pass


class Response(requests.Response):
    """The :class:`Response <Response>` object, which contains a
    server's response to an HTTP request.
    """

    __attrs__ = [
        '_content',
        'status_code',
        'headers',
        'url',
        'history',
        'encoding',
        'reason',
        'cookies',
        'elapsed',
        'request',
        'meta'
    ]

    meta = None

    def __init__(self, response=None):
        if isinstance(response, requests.Response):
            self.__dict__ = response.__dict__

    def __len__(self):
        if 'content-length' in self.headers:
            return int(self.headers['content-length'])
        return self.raw.tell()

    @property
    def body(self):
        return self.content

    @property
    def status(self):
        return self.status_code
