#

import os
import sys
import socket
import time
import xmlrpc.client
import urllib3

from up2date_client import config
from up2date_client import up2dateLog
from up2date_client import up2dateErrors
from up2date_client import up2dateUtils

from rhn import rpclib

try:  # python2
    import httplib
    import urllib2
    import urlparse
    import xmlrpclib
except ImportError:  # python3
    import http.client as httplib
    import urllib.request as urllib2
    import urllib.parse as urlparse
    import xmlrpc.client as xmlrpclib

import gettext

t = gettext.translation("rhn-client-tools", fallback=True)
# Python 3 translations don't have a ugettext method
if not hasattr(t, "ugettext"):
    t.ugettext = t.gettext
_ = t.ugettext


def stdoutMsgCallback(msg):
    print(msg)


class RetryServer(rpclib.Server):
    # Dict of server: error_message pairs to keep track of failure messages and display them if needed
    _error_messages = {}

    def addServerList(self, serverList):
        self.serverList = serverList

    def _request1(self, methodname, params):
        self.log = up2dateLog.initLog()

        while 1:
            try:
                ret = super()._ServerProxy__request(methodname, params)
            except xmlrpclib.Fault:
                raise
            except httplib.BadStatusLine:
                self.log.log_me("Error: Server Unavailable. Please try later.")
                stdoutMsgCallback(_("Error: Server Unavailable. Please try later."))
                sys.exit(-1)
            except:
                server = self.serverList.next()

                if server is None:
                    # since just because we failed, the server list could
                    # change (aka, firstboot, they get an option to reset the
                    # the server configuration) so reset the serverList
                    self.serverList.resetServerIndex()

                    # Display the error messages for all servers to stdout
                    # This only happens if all servers in the list fail
                    if self._error_messages:
                        error_combined_msg = "\n".join(
                            ["%s:\n%s" % (host, error) for host, error in self._error_messages.items()]
                        )
                        # No need to log this, these are already logged at the same point where the error occurred
                        stdoutMsgCallback(_("Errors occurred while trying to connect to the remote servers."))
                        stdoutMsgCallback(error_combined_msg)

                    raise

                msg = "An error occurred talking to %s:\n" % self._host
                exception_msg = "%s\n%s\n" % (sys.exc_info()[0], sys.exc_info()[1])

                # Store the error message for the failed server
                self._error_messages[self._host] = exception_msg

                msg = msg + exception_msg
                msg = msg + "Trying the next serverURL: %s\n" % self.serverList.server()

                self.log.log_me(msg)
                # try a different url

                # use the next serverURL
                parse_res = urlparse.urlsplit(self.serverList.server())
                typ = parse_res[0]  # scheme
                self._host = parse_res[1]  # netloc
                self._handler = parse_res[2]  # path
                typ = typ.lower()
                if typ not in ("http", "https"):
                    raise rpclib.InvalidRedirectionError("Redirected to unsupported protocol %s" % typ)
                self._orig_handler = self._handler
                self._type = typ
                self._uri = self.serverList.server()
                if not self._handler:
                    self._handler = "/RPC2"
                self._allow_redirect = 1
                continue
            # if we get this far, we succedded
            break
        return ret

    def __getattr__(self, name):
        # magic method dispatcher
        return xmlrpc.client._Method(self._request1, name)


# uh, yeah, this could be an iterator, but we need it to work on 1.5 as well
class ServerList:
    def __init__(self, serverlist=[]):
        self.serverList = serverlist
        self.index = 0

    def server(self):
        self.serverurl = self.serverList[self.index]
        return self.serverurl

    def next(self):
        self.index = self.index + 1
        if self.index >= len(self.serverList):
            return None
        return self.server()

    def resetServerIndex(self):
        self.index = 0


def getServer(
    serverOverride=None,
    timeout=None,
    registration=False,
):
    log = up2dateLog.initLog()
    cfg = config.initUp2dateConfig()

    # it is known that CLN sertificate is a bit special, so just disable warnings
    urllib3.disable_warnings(urllib3.exceptions.SubjectAltNameWarning)

    # Where do we keep the CA certificate for RHNS?
    # The servers we're talking to need to have their certs
    # signed by one of these CA.
    ca = cfg["sslCACert"]
    if isinstance(ca, list):
        raise up2dateErrors.ValidationError("SSL certificate must be a string")

    rhns_ca_cert = ca or "/usr/share/rhn/CLN-ORG-TRUSTED-SSL-CERT"
    if cfg["enableProxy"]:
        proxyHost = config.getProxySetting()
    else:
        proxyHost = None

    if not serverOverride:
        serverUrls = config.getServerURL(registration=registration)

        # Check for a fallback server URL and add it to the list if available
        fallbackURL = config.getFallbackServerURL(registration=registration)
        # Ensure that none of the URLs from the fallback list are already in the serverUrls list
        if fallbackURL:
            for url in fallbackURL:
                if url not in serverUrls:
                    serverUrls.append(url)
    else:
        serverUrls = serverOverride
    serverList = ServerList(serverUrls)

    proxyUser = None
    proxyPassword = None
    if cfg["enableProxyAuth"]:
        proxyUser = cfg["proxyUser"] or None
        proxyPassword = cfg["proxyPassword"] or None

    s = RetryServer(
        serverList.server(),
        proxy=proxyHost,
        username=proxyUser,
        password=proxyPassword,
        timeout=timeout
    )
    s.addServerList(serverList)

    s.add_header("X-Up2date-Version", up2dateUtils.version())

    # require CLN-ORG-TRUSTED-SSL-CERT file to be able to authenticate the SSL connections
    need_ca = [True for i in s.serverList.serverList if urlparse.urlparse(i)[0] == "https"]
    if need_ca:
        if not os.access(rhns_ca_cert, os.R_OK):
            msg = "%s: %s" % (_("ERROR: can not find RHNS CA file"),
                                 rhns_ca_cert)
            log.log_me("%s" % msg)
            raise up2dateErrors.SSLCertificateFileNotFound(msg)

        # force the validation of the SSL cert
        s.set_trusted_cert(rhns_ca_cert)

    return s


def doCall(method, *args, **kwargs):
    log = up2dateLog.initLog()
    log.log_debug("rpcServer: Calling XMLRPC %s" % method.__dict__["_Method__name"])
    cfg = config.initUp2dateConfig()
    ret = None

    attempt_count = 1
    try:
        attempts = int(cfg["networkRetries"])
    except ValueError:
        attempts = 1
    if attempts <= 0:
        attempts = 1

    while 1:
        failure = 0
        ret = None
        try:
            ret = method(*args, **kwargs)
        except KeyboardInterrupt:
            raise up2dateErrors.CommunicationError(_("Connection aborted by the user"))
        # if we get a socket error, keep tryingx2
        except socket.error as e:
            log.log_me("A socket error occurred: %s, attempt #%s" % (
                sys.exc_info()[1], attempt_count))
            if attempt_count >= attempts:
                e = sys.exc_info()[1]
                # CLOS-4607: stringify the message before building the error.
                # requests' SSLError/ConnectionError carry a urllib3 MaxRetryError
                # object as e.args[0]; up2dateErrors -> i18n.ustr() then raised
                # "TypeError: ... need a bytes-like object" on it, masking the real
                # SSL error (e.g. CERTIFICATE_VERIFY_FAILED) from the user.
                if len(e.args) > 1:
                    raise up2dateErrors.CommunicationError(str(e.args[1]))
                else:
                    raise up2dateErrors.CommunicationError(
                        str(e.args[0]) if e.args else str(e))
            else:
                failure = 1
        except httplib.IncompleteRead:
            print("httplib.IncompleteRead")
            raise up2dateErrors.CommunicationError("httplib.IncompleteRead")

        except urllib2.HTTPError:
            e = sys.exc_info()[1]
            msg = "\nAn HTTP error occurred:\n"
            msg = msg + "URL: %s\n" % e.filename
            msg = msg + "Status Code: %s\n" % e.code
            msg = msg + "Error Message: %s\n" % e.msg
            log.log_me(msg)
            raise up2dateErrors.CommunicationError(msg)

        except xmlrpclib.ProtocolError:
            e = sys.exc_info()[1]
            log.log_me("A protocol error occurred: %s , attempt #%s," % (e.errmsg, attempt_count))
            if e.errcode == 404:
                log.log_me("Could not find URL, %s" % (e.url))
                log.log_me("Check server name and/or URL, then retry\n")

            (errCode, errMsg) = rpclib.reportError(e.headers)
            reset = 0
            if abs(errCode) == 34:
                log.log_me("Auth token timeout occurred\n errmsg: %s" % errMsg)
                # this calls login, which in tern calls doCall (ie,
                # this function) but login should never get a 34, so
                # should be safe from recursion

                from up2date_client import up2dateAuth

                up2dateAuth.updateLoginInfo()

            # the servers are being throttle to pay users only, catch the
            # exceptions and display a nice error message
            if abs(errCode) == 51:
                log.log_me(_("Server has refused connection due to high load"))
                raise up2dateErrors.CommunicationError(e.errmsg)
            # if we get a 404 from our server, thats pretty
            # fatal... no point in retrying over and over. Note that
            # errCode == 17 is specific to our servers, if the
            # serverURL is just pointing somewhere random they will
            # get a 0 for errcode and will raise a CommunicationError
            if abs(errCode) == 17:
                # in this case, the args are the package string, so lets try to
                # build a useful error message
                if type(args[0]) == type([]):
                    pkg = args[0]
                else:
                    pkg = args[1]

                if type(pkg) == type([]):
                    pkgName = "%s-%s-%s.%s" % (pkg[0], pkg[1], pkg[2], pkg[4])
                else:
                    pkgName = pkg
                msg = "File Not Found: %s\n%s" % (pkgName, errMsg)
                log.log_me(msg)
                raise up2dateErrors.FileNotFoundError(msg)

            if not reset:
                if attempt_count >= attempts:
                    raise up2dateErrors.CommunicationError(e.errmsg)
                else:
                    failure = 1

        except xmlrpclib.ResponseError:
            raise up2dateErrors.CommunicationError("Broken response from the server.")

        if ret != None:
            break
        else:
            failure = 1

        if failure:
            # rest for five seconds before trying again
            time.sleep(5)
            attempt_count = attempt_count + 1

        if attempt_count > attempts:
            raise up2dateErrors.CommunicationError("The data returned from the server was incomplete")

    return ret
