from __future__ import absolute_import
import sys
import os
import re
import traceback
import requests
import socket
from . import threadutils
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import mimetypes
from . import plexobjects
from xml.etree import ElementTree

from . import asyncadapter

from . import callback
from . import util


codes = requests.codes
status_codes = requests.status_codes._codes


DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(10).setConnectTimeout(10)


def GET(*args, **kwargs):
    return requests.get(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs)


def POST(*args, **kwargs):
    return requests.post(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs)


def Session():
    s = asyncadapter.Session()
    s.headers = util.BASE_HEADERS.copy()
    s.timeout = util.TIMEOUT

    return s


class RequestContext(dict):
    def __getattr__(self, attr):
        return self.get(attr)

    def __setattr__(self, attr, value):
        self[attr] = value


class HttpRequest(object):
    _cancel = False

    def __init__(self, url, method=None, forceCertificate=False):
        self.server = None
        self.path = None
        self.hasParams = '?' in url
        self.ignoreResponse = False
        self.session = asyncadapter.Session()
        self.session.headers = util.BASE_HEADERS.copy()
        self.currentResponse = None
        self.method = method
        self.url = url
        self.thread = None

        # Use our specific plex.direct CA cert if applicable to improve performance
        # if forceCertificate or url[:5] == "https":  # TODO: ---------------------------------------------------------------------------------IMPLEMENT
        #     certsPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'certs')
        #     if "plex.direct" in url:
        #         self.session.cert = os.path.join(certsPath, 'plex-bundle.crt')
        #     else:
        #         self.session.cert = os.path.join(certsPath, 'ca-bundle.crt')

    def removeAsPending(self):
        from . import plexapp
        util.APP.delRequest(self)

    def startAsync(self, *args, **kwargs):
        self.thread = threadutils.KillableThread(target=self._startAsync, args=args, kwargs=kwargs, name='HTTP-ASYNC:{0}'.format(self.url))
        self.thread.start()
        return True

    def _startAsync(self, body=None, contentType=None, context=None):
        timeout = context and context.timeout or DEFAULT_TIMEOUT
        self.logRequest(body, timeout)
        if self._cancel:
            return
        try:
            if self.method == 'PUT':
                res = self.session.put(self.url, timeout=timeout, stream=True)
            elif self.method == 'DELETE':
                res = self.session.delete(self.url, timeout=timeout, stream=True)
            elif self.method == 'HEAD':
                res = self.session.head(self.url, timeout=timeout, stream=True)
            elif self.method == 'POST' or body is not None:
                if not contentType:
                    self.session.headers["Content-Type"] = "application/x-www-form-urlencoded"
                else:
                    self.session.headers["Content-Type"] = mimetypes.guess_type(contentType)

                res = self.session.post(self.url, data=body or None, timeout=timeout, stream=True)
            else:
                res = self.session.get(self.url, timeout=timeout, stream=True)
            self.currentResponse = res

            if self._cancel:
                return
        except asyncadapter.TimeoutException:
            from . import plexapp
            plexapp.util.APP.onRequestTimeout(context)
            self.removeAsPending()
            return
        except Exception as e:
            util.ERROR('Request failed {0}'.format(util.cleanToken(self.url)), e)
            if not hasattr(e, 'response'):
                return
            res = e.response

        self.onResponse(res, context)

        self.removeAsPending()

    def getWithTimeout(self, seconds=DEFAULT_TIMEOUT):
        return HttpObjectResponse(self.getPostWithTimeout(seconds), self.path, self.server)

    def postWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None):
        self.method = 'POST'
        return HttpObjectResponse(self.getPostWithTimeout(seconds, body), self.path, self.server)

    def getToStringWithTimeout(self, seconds=DEFAULT_TIMEOUT):
        res = self.getPostWithTimeout(seconds)
        if not res:
            return ''
        return res.text.encode('utf8')

    def postToStringWithTimeout(self, body=None, seconds=DEFAULT_TIMEOUT):
        self.method = 'POST'
        res = self.getPostWithTimeout(seconds, body)
        if not res:
            return ''
        return res.text.encode('utf8')

    def getPostWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None):
        if self._cancel:
            return

        self.logRequest(body, seconds, False)
        try:
            if self.method == 'PUT':
                res = self.session.put(self.url, timeout=seconds, stream=True)
            elif self.method == 'DELETE':
                res = self.session.delete(self.url, timeout=seconds, stream=True)
            elif self.method == 'HEAD':
                res = self.session.head(self.url, timeout=seconds, stream=True)
            elif self.method == 'POST' or body is not None:
                res = self.session.post(self.url, data=body, timeout=seconds, stream=True)
            else:
                res = self.session.get(self.url, timeout=seconds, stream=True)

            self.currentResponse = res

            if self._cancel:
                return None

            util.LOG("Got a {0} from {1}".format(res.status_code, util.cleanToken(self.url)))
            # self.event = msg
            return res
        except Exception as e:
            info = traceback.extract_tb(sys.exc_info()[2])[-1]
            util.WARN_LOG(
                "Request errored out - URL: {0} File: {1} Line: {2} Msg: {3}".format(util.cleanToken(self.url), os.path.basename(info[0]), info[1], e.message)
            )

        return None

    def wasOK(self):
        return self.currentResponse and self.currentResponse.ok

    def wasNotFound(self):
        return self.currentResponse is not None and self.currentResponse.status_code == requests.codes.not_found

    def getIdentity(self):
        return str(id(self))

    def getUrl(self):
        return self.url

    def getRelativeUrl(self):
        url = self.getUrl()
        m = re.match('^\w+:\/\/.+?(\/.+)', url)
        if m:
            return m.group(1)
        return url

    def killSocket(self):
        if not self.currentResponse:
            return

        try:
            socket.fromfd(self.currentResponse.raw.fileno(), socket.AF_INET, socket.SOCK_STREAM).shutdown(socket.SHUT_RDWR)
            return
        except AttributeError:
            pass
        except Exception as e:
            util.ERROR(err=e)

        try:
            self.currentResponse.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR)
        except AttributeError:
            pass
        except Exception as e:
            util.ERROR(err=e)

    def cancel(self):
        self._cancel = True
        self.session.cancel()
        self.removeAsPending()
        self.killSocket()

    def addParam(self, encodedName, value):
        if self.hasParams:
            self.url += "&" + encodedName + "=" + six.moves.urllib.parse.quote_plus(value)
        else:
            self.hasParams = True
            self.url += "?" + encodedName + "=" + six.moves.urllib.parse.quote_plus(value)

    def addHeader(self, name, value):
        self.session.headers[name] = value

    def createRequestContext(self, requestType, callback_=None):
        context = RequestContext()
        context.requestType = requestType
        context.timeout = DEFAULT_TIMEOUT

        if callback_:
            context.callback = callback.Callable(self.onResponse)
            context.completionCallback = callback_
            context.callbackCtx = callback_.context

        return context

    def onResponse(self, event, context):
        if context.completionCallback:
            response = HttpResponse(event)
            context.completionCallback(self, response, context)

    def logRequest(self, body, timeout=None, _async=True):
        # Log the real request method
        method = self.method
        if not method:
            method = body is not None and "POST" or "GET"
        util.LOG(
            "Starting request: {0} {1} (async={2} timeout={3})".format(method, util.cleanToken(self.url),
                                                                       _async, timeout)
        )


class HttpResponse(object):
    def __init__(self, event):
        self.event = event
        if not self.event is None:
            self.event.content  # force data to be read
            self.event.close()

    def isSuccess(self):
        if not self.event:
            return False
        return self.event.status_code >= 200 and self.event.status_code < 300

    def isError(self):
        return not self.isSuccess()

    def getStatus(self):
        if self.event is None:
            return 0
        return self.event.status_code

    def getBodyString(self):
        if self.event is None:
            return ''
        return self.event.text.encode('utf-8')

    def getErrorString(self):
        if self.event is None:
            return ''
        return self.event.reason

    def getBodyXml(self):
        if not self.event is None:
            return ElementTree.fromstring(self.getBodyString())

        return None

    def getResponseHeader(self, name):
        if self.event is None:
            return None
        return self.event.headers.get(name)


class HttpObjectResponse(HttpResponse, plexobjects.PlexContainer):
    def __init__(self, response, path, server=None):
        self.event = response
        if self.event:
            self.event.content  # force data to be read
            self.event.close()

        data = self.getBodyXml()

        plexobjects.PlexContainer.__init__(self, data, initpath=path, server=server, address=path)
        self.container = self

        self.items = plexobjects.listItems(server, path, data=data, container=self)


def addRequestHeaders(transferObj, headers=None):
    if isinstance(headers, dict):
        for header in headers:
            transferObj.addHeader(header, headers[header])
            util.DEBUG_LOG("Adding header to {0}: {1}: {2}".format(transferObj, header, headers[header]))


def addUrlParam(url, param):
    return url + ('?' in url and '&' or '?') + param
