# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
=============
TAP plus
=============

"""
import os.path

import requests
from six.moves.urllib.parse import urlencode
from six.moves import http_client as httplib

from astroquery.utils.tap.core import TapPlus
from astroquery.cadc.cadctap.tapconn import TapConnCadc
from astroquery.cadc.cadctap.jobSaxParser import JobSaxParserCadc
from astroquery.utils.tap import taputils


__all__ = ['TapPlusCadc']

VERSION = "1.0.1"
TAP_CLIENT_ID = "aqtappy-" + VERSION


class TapPlusCadc(TapPlus):
    """TAP plus class
    Provides TAP and TAP+ capabilities

    Notes
    -----------------
    To override some parts of the utils/tap library in order for it to
    function with the CADC's implementation of TAP
    """

    def __init__(self, url=None, host=None, server_context=None,
                 tap_context=None, port=80, sslport=443,
                 default_protocol_is_https=False, connhandler=None,
                 verbose=True):
        """

        Parameters
        ----------
        url : str, mandatory if no host is specified, default None
            TAP URL
        host : str, optional, default None
            host name
        server_context : str, optional, default None
            server context
        tap_context : str, optional, default None
            tap context
        port : int, optional, default '80'
            HTTP port
        sslport : int, optional, default '443'
            HTTPS port
        default_protocol_is_https : bool, optional, default False
            Specifies whether the default protocol to be used is HTTPS
        connhandler connection handler object, optional, default None
            HTTP(s) connection hander (creator). If no handler is provided, a
            new one is created.
        verbose : bool, optional, default 'True'
            flag to display information about the process

        Notes
        -----------------
        In order to use the new version of TapConn which is TapConnCadc it
        had to be passed in at this level instead of being created in the
        original __init__() which would create the old TapConn. TapConnCadc
        does redirects and uses certificates.
        """
        if connhandler is not None:
            connHandler = connhandler
        elif url is not None:
            protocol, host, port, server_context, \
                tap_context = self._Tap__parseUrl(url)
            if protocol == "http":
                connHandler = TapConnCadc(False,
                                          host=host,
                                          server_context=server_context,
                                          tap_context=tap_context,
                                          port=port,
                                          sslport=sslport)
            else:
                # https port -> sslPort
                connHandler = TapConnCadc(True,
                                          host=host,
                                          server_context=server_context,
                                          tap_context=tap_context,
                                          port=port,
                                          sslport=port)
        else:
            connHandler = TapConnCadc(default_protocol_is_https,
                                      host=host,
                                      server_context=server_context,
                                      tap_context=tap_context,
                                      port=port,
                                      sslport=sslport)
        self.__certificate = None
        super(TapPlusCadc, self).__init__(url=url, host=host,
                                          server_context=server_context,
                                          tap_context=tap_context, port=port,
                                          sslport=sslport,
                                          default_protocol_is_https=default_protocol_is_https,

                                          connhandler=connHandler,
                                          verbose=verbose)

    def load_table(self, table, verbose=False):
        """Loads the specified table

        Parameters
        ----------
        table : str, mandatory
            full qualified table name (i.e. schema name + table name)
        verbose : bool, optional, default 'False'
            flag to display information about the process

        Returns
        -------
        A table object

        Notes
        -----------------
        TAP load table sends parameter '?table=tablename' to the /tables url
        but this isn't available for all implementations
        """
        if verbose:
            print("Retrieving table '"+str(table)+"'")
        tables = self._Tap__load_tables(only_names=False,
                                        include_shared_tables=False,
                                        verbose=verbose)
        for tab in tables:
            if tab.get_qualified_name() == table:
                return tab
        return

    def _Tap__launchJobMultipart(self, query, uploadResource, uploadTableName,
                                 outputFormat, context, verbose, name=None,
                                 autorun=True):
        """

        Notes
        -----------------
        Get rid of PHASE:RUN in args and if async job send PHASE:RUN to
        async/phase in order to get the job to run
        """
        uploadValue = str(uploadTableName) + ",param:" + str(uploadTableName)
        args = {
            "REQUEST": "doQuery",
            "LANG": "ADQL",
            "FORMAT": str(outputFormat),
            "tapclient": str(TAP_CLIENT_ID),
            "QUERY": str(query),
            "UPLOAD": ""+str(uploadValue)}
        f = open(uploadResource, "r")
        chunk = f.read()
        f.close()
        files = [[uploadTableName, uploadResource, chunk]]
        contentType, body = self._Tap__connHandler.encode_multipart(args,
                                                                    files)
        response = self._Tap__connHandler.execute_tappost(context, body,
                                                          contentType)
        if verbose:
            print(response.status, response.reason)
            print(response.getheaders())
        if 'async' in context:
            # Check for error
            location = self._Tap__connHandler.find_header(
                response.getheaders(),
                "location")
            jobid = taputils.get_jobid_from_location(location)
            runresponse = self.__runAsyncQuery(jobid, verbose)
            return runresponse
        return response

    def _Tap__launchJob(self, query, outputFormat,
                        context, verbose, name=None, autorun=True):
        """

        Notes
        -----------------
        Get rid of PHASE:RUN in args and if async job send PHASE:RUN to
        async/phase in order to get the job to run
        """
        args = {
            "REQUEST": "doQuery",
            "LANG": "ADQL",
            "FORMAT": str(outputFormat),
            "tapclient": str(TAP_CLIENT_ID),
            "QUERY": str(query)}
        data = self._Tap__connHandler.url_encode(args)
        response = self._Tap__connHandler.execute_tappost(context, data)
        if verbose:
            print(response.status, response.reason)
            print(response.getheaders())
        if 'async' in context:
            location = self._Tap__connHandler.find_header(
                response.getheaders(),
                "location")
            jobid = taputils.get_jobid_from_location(location)
            runresponse = self.__runAsyncQuery(jobid, verbose)
            return runresponse
        return response

    def __runAsyncQuery(self, jobid, verbose):
        """
        Notes
        ------------------
        This was added in order to run the job after it was created
        """
        args = {
            "PHASE": "RUN"}
        data = self._Tap__connHandler.url_encode(args)
        response = self._Tap__connHandler.execute_tappost('async/'+jobid+'/phase',
                                                          data)
        if verbose:
            print(response.status, response.reason)
            print(response.getheaders())
        return response

    def load_async_job(self, jobid=None, verbose=False):
        """Loads an asynchronous job

        Parameters
        ----------
        jobid : str, mandatory if no name is provided, default None
            job identifier
        verbose : bool, optional, default 'False'
            flag to display information about the process
        Returns
        -------
        A Job object

        Notes
        -----------------
        In order to output a JobCadc instead of just a Job object, and
        to add more information to the JobCadc becuase it causes errors
        when saving if you don't set the format
        """
        if jobid is None:
            if verbose:
                print("No job identifier found")
            return
        subContext = "async/" + str(jobid)
        response = self._Tap__connHandler.execute_tapget(subContext)
        if verbose:
            print(response.status, response.reason)
            print(response.getheaders())
        isError = self._Tap__connHandler.check_launch_response_status(response,
                                                                      verbose,
                                                                      200)
        if isError:
            if verbose:
                print(response.reason)
            raise requests.exceptions.HTTPError(response.reason)
            return
        # parse job
        jsp = JobSaxParserCadc(async_job=True)
        job = jsp.parseData(response)[0]
        job.connHandler = self._Tap__connHandler
        suitableOutputFile = self._Tap__getSuitableOutputFile(
            job.async_,
            None,
            response.getheaders(),
            isError,
            job.parameters['FORMAT'])
        job.outputFile = suitableOutputFile
        job.parameters['format'] = job.parameters['FORMAT']
        # load results
        job.get_results(verbose)
        return job

    def save_results(self, job, filename, verbose=False):
        """Saves job results

        Parameters
        ----------
        job : Job, mandatory
            job
        verbose : bool, optional, default 'False'
            flag to display information about the process

        Notes
        -----------------
        To send in the filename to the new save_results function in JobCadc
        """
        job.save_results(filename, verbose)

    def login(self, user=None, password=None, certificate_file=None,
              cookie_prefix=None, login_url=None, verbose=False):
        """Performs a login.
        User and password can be used or a file that contains user name and
        password
        (2 lines: one for user name and the following one for the password)

        Parameters
        ----------
        user : str, mandatory if 'file' is not provided, default None
            login name
        password : str, mandatory if 'file' is not provided, default None
            user password
        certificate_file : str, mandatory if no 'user' & 'password' is provided
            location of the certificate
        verbose : bool, optional, default 'False'
            flag to display information about the process

        Notes
        -----------------
        Add certificates to the login
        """
        if certificate_file is None and user is None:
            raise AttributeError(
                'Input user/password or certificate file path')
        if certificate_file is not None and user is not None:
            raise AttributeError(
                'Choose one form of authentication only')
        if certificate_file is not None:
            print(dir(self._TapPlus__getconnhandler().cookies_set))
            if self._TapPlus__getconnhandler().cookies_set():
                raise AttributeError('Already logged in with user/password')
            if not os.path.isfile(certificate_file):
                raise AttributeError('File does not exist - '+certificate_file)
            else:
                self._TapPlus__getconnhandler()._TapConn__connectionHandler. \
                    _ConnectionHandlerCadc__certificate = certificate_file
                self.__certificate = certificate_file
                self._TapPlus__isLoggedIn = True
                return
        if user is not None and password is not None:
            if self.__certificate is not None:
                raise AttributeError('Already logged in with certificate')
            self._TapPlus__user = user
            self._TapPlus__pwd = password
            self.__dologin(cookie_prefix=cookie_prefix,
                           login_url=login_url, verbose=verbose)
        else:
            if password is None:
                raise AttributeError("No password")

    def __dologin(self, cookie_prefix=None, login_url=None, verbose=False):
        """
        Notes
        -----------------
        Decode and set the cookie with the right key
        """
        self._TapPlus__isLoggedIn = False
        response = self.__execLogin(self._TapPlus__user,
                                    self._TapPlus__pwd,
                                    login_url,
                                    verbose)
        # check response
        connHandler = self._TapPlus__getconnhandler()
        isError = connHandler.check_launch_response_status(response,
                                                           verbose,
                                                           200)
        if isError:
            if verbose:
                print("Login error: " + str(response.reason))
            raise requests.exceptions.HTTPError(
                "Login error: " + str(response.reason))
        else:
            # extract cookie
            cookie = response.read()
            c = cookie.decode()
            cookie = '{}="{}"'.format(cookie_prefix, c)
            if cookie is not None:
                self._TapPlus__isLoggedIn = True
                connHandler.set_cookie(cookie)

    def logout(self, verbose=False):
        """Performs a logout

        Parameters
        ----------
        verbose : bool, optional, default 'False'
            flag to display information about the process

        Notes
        -----------------
        Clear the certificate as well
        """
        self._TapPlus__getconnhandler()._TapConn__connectionHandler. \
            _ConnectionHandlerCadc__certificate = None
        if self._TapPlus__getconnhandler().cookies_set():
            self._TapPlus__getconnhandler().unset_cookie()
        self.__certificate = None
        self._TapPlus__isLoggedIn = False

    def __execLogin(self, usr, pwd, login_url=None, verbose=False):
        """
        Notes
        -----------------
        Send to the right login url, send not using the TapConn object
        because it redirects instead of giving you a cookie
        """
        args = {
            "username": str(usr),
            "password": str(pwd)}
        data = urlencode(args)
        if login_url is None:
            raise RuntimeError("No login URL")
        protocol, host, port, server_context, \
            tap_context = self._Tap__parseUrl(login_url)
        connHandler = httplib.HTTPConnection(host, 80)
        context = '/ac/login'
        header = {
            "Content-type": "application/x-www-form-urlencoded",
            "Accept": "text/plain"
        }
        connHandler.request("POST", context, data, header)
        response = connHandler.getresponse()
        if(verbose):
            print(response.status, response.reason)
            print(response.getheaders())
        return response
