import sys
import os
import re
import traceback
import requests
import socket
import threadutils
import urllib
import mimetypes
import plexobjects
from xml.etree import ElementTree

import asyncadapter

import callback
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):
        import plexapp
        plexapp.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:
            import plexapp
            plexapp.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 + "=" + urllib.quote_plus(value)
        else:
            self.hasParams = True
            self.url += "?" + encodedName + "=" + urllib.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
