import os
import time
import tempfile
import getpass

import json

import requests
try:
    import socks
except ImportError:
    socks = None
try:
    from urlparse import urlparse
except ImportError:
    from urllib.parse import urlparse
from .select import Select
from .help import Inspector, GraphData, PaintGraph, _DRAW_GRAPHS
from .manage import GlobalManager
from .uriutil import join_uri, file_path, uri_last
from .jsonutil import csv_to_json
from .errors import is_xnat_error
from .errors import catch_error
from .array import ArrayData
from .xpath_store import XpathStore
from . import xpass


DEBUG = False


# main entry point
class Interface(object):
    """ Main entry point to access a XNAT server.

        >>> central = Interface(server='http://central.xnat.org:8080',
                                user='login',
                                password='pwd',
                                cachedir='/tmp')

        Or with config file:

        >>> central = Interface(config='/home/me/.xnat.cfg')

        Or for interactive use:

        >>> central = Interface('http://central.xnat.org')

        .. note::
            The interactive mode is activated whenever an argument within
            server, user or password is missing. In interactive mode pyxnat
            tries to check the validity of the connection parameters.

        Or anonymously (unauthenticated):

        >>> central = Interface('http://central.xnat.org', anonymous=True)

        Attributes
        ----------
        _mode: online | offline
            Online or offline mode
        _memtimeout: float
            Lifespan of in-memory cache

        .. note::
            Proxy support requires the socks module be installed. This can be
            installed via pip:
            pip install SocksiPy-branch

        .. note::
            All caching functionality has been removed from pyxnat as of 1.0.0.0.
            The cache was causing more hassle than it was worth.

    """

    def __init__(self, server=None, user=None, password=None,
                 cachedir=tempfile.gettempdir(), config=None,
                 anonymous=False, proxy=None, verify=None):
        """
            Parameters
            ----------
            server: string | None
                The server full URL (including port and XNAT instance name
                if necessary) e.g. http://central.xnat.org,
                http://localhost:8080/xnat_db
                Or a path to an existing config file. In that case the other
                parameters (user etc..) are ignored if given.
                If None the user will be prompted for it.
            user: string | None
                A valid login registered through the XNAT web interface.
                If None the user will be prompted for it.
            password: string | None
                The user's password.
                If None the user will be prompted for it.
            cachedir: string  (Depricated)

            config: string
                Reads a config file in json to get the connection parameters.
                If a config file is specified, it will be used regardless of
                other parameters that might have been given.
            anonymous: boolean
                Indicates an unauthenticated connection.  If True, user
                and password are ignored and a session is started with
                no credentials.
            proxy: string | None
                Indicates the full URL for an HTTP proxy server to be used for
                transactions with the specified XNAT server. If you need to
                specify a username and password for proxy access, prepend them
                to the hostname:
                http://user:pass@hostname:port

            verify: True, False, or path to file containing certificate for your site
              Added to the requests Session, as documented here:
              http://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification
              Simplifies handling self-certified sites, or sites where there is an issue
              with certification

        """

        self._interactive = False

        self._anonymous = anonymous

        self._verify = verify

        if self._anonymous:

            if server is None:
                self._server = raw_input('Server: ')
                self._interactive = True
            else:
                self._server = server
                self._interactive = False

            self.__set_proxy(proxy)

            self._user = None
            self._pwd = None

        else:

            if not all([server, user, password]) and not config:
                self._interactive = True

            if all(arg is None
                    for arg in [server, user, password, config]) \
                    and os.path.exists(xpass.path()):

                connection_args = xpass.read_xnat_pass(xpass.path())

                if connection_args is None:
                    raise Exception('XNAT configuration file not found '
                                    'or formatted incorrectly.')

                self._server = connection_args['host']
                self._user = connection_args['u']
                self._pwd = connection_args['p']


                if 'proxy' in connection_args:
                    self.__set_proxy(connection_args['proxy'])
                else:
                    self.__set_proxy(None)

            elif config is not None:
                self.load_config(config)

            else:
                if server is None:
                    self._server = raw_input('Server: ')
                else:
                    self._server = server

                if user is None:
                    user = raw_input('User: ')

                if password is None:
                    password = getpass.getpass()

                self._user = user
                self._pwd = password

                self.__set_proxy(proxy)

        self._callback = None

        self._memcache = {}
        self._memtimeout = 1.0
        self._mode = 'online'
        self._struct = {}
        self._entry = None

        self._last_memtimeout = 1.0
        self._last_mode = 'online'

        self._jsession = 'authentication_by_credentials'
        self._connect_extras = {}
        self._connect()

        self.inspect = Inspector(self)
        self.select = Select(self)
        self.array = ArrayData(self)
        # self.cache = CacheManager(self)
        self.manage = GlobalManager(self)
        self.xpath = XpathStore(self)

        if _DRAW_GRAPHS:
            self._get_graph = GraphData(self)
            self.draw = PaintGraph(self)

        if self._interactive:
            self._get_entry_point()

        self.inspect()

    def __getstate__(self):
        return {
            '_server': self._server,
            '_user': self._user,
            '_pwd': self._pwd,
            '_anonymous': self._anonymous,
        }

    def __setstate__(self, dictionary):
        self.__dict__ = dictionary
        if self._anonymous:
            self.__init__(self._server, anonymous=True)
        else:
            self.__init__(self._server, self._user, self._pwd)

    def __set_proxy(self, proxy=None):
        if proxy is None:
            proxy = os.environ.get("http_proxy")
        if proxy is None:
            self._proxy_url = None
        else:
            self._proxy_url = urlparse(proxy)

    def _get_entry_point(self):
        if self._entry is None:
            # /REST for XNAT 1.4, /data if >=1.5
            self._entry = '/REST'
            try:
                self._jsession = 'JSESSIONID=' + self._exec('/data/JSESSION', force_preemptive_auth=True)
                self._entry = '/data'

                if is_xnat_error(self._jsession):
                    catch_error(self._jsession)
            except Exception as e:
                if not '/data/JSESSION' in str(e):
                    raise e

        return self._entry

    def _connect(self, **kwargs):
        """ Sets up the connection with the XNAT server.

            Parameters
            ----------
            kwargs: dict
                Can be used to pass additional arguments to
                the Http constructor. See the httplib2 documentation
                for details. http://code.google.com/p/httplib2/
        """

        if kwargs != {}:
            self._connect_extras = kwargs
        else:
            kwargs = self._connect_extras

        self._http = requests.Session()

        # requests verify defaults to True, but can be set from environment variables
        # Leave as-is unless user has explicitly overridden it
        if self._verify is not None:
          self._http.verify = self._verify

        if not self._anonymous:
            self._http.auth = (self._user, self._pwd)

        if self._proxy_url:
            self._http.proxies = {'http': self._proxy_url.geturl()}


        # Turns out this doesn't work any more: XNAT doesn't do the 401 response that forces
        # httplib2 to re-submit the request with credentials. See where the Authorization header
        # is added manually in the _exec function.
        # if not self._anonymous:
        #    self._http.add_credentials(self._user, self._pwd)

    def _exec(self, uri, method='GET', body=None, headers=None, force_preemptive_auth=False, **kwargs):
        """ A wrapper around a simple httplib2.request call that:
                - avoids repeating the server url in the request
                - deals with custom caching mechanisms :: Depricated
                - manages a user session with cookies
                - catches and broadcast specific XNAT errors

            Parameters
            ----------
            uri: string
                URI of the resource to be accessed. e.g. /REST/projects
            method: GET | PUT | POST | DELETE | HEAD
                HTTP method.
            body: string | dict
                HTTP message body
            headers: dict
                Additional headers for the HTTP request.
            force_preemptive_auth: boolean
                .. note:: Depricated as of 1.0.0.0
                Indicates whether the request should include an Authorization header with basic auth credentials.
            **kwargs: dictionary
                Additional parameters to pass directly to the Requests HTTP call.

            HTTP:GET
            ----------
                When calling with GET as method, the body parameter can be a key:value dictionary containing
                request parameters or a string of parameters. They will be url encoded and appended to the url.

            HTTP:POST
            ----------
                When calling with POST as method, the body parameter can be a key:value dictionary containing
                request parameters they will be url encoded and appended to the url.

        """

        if headers is None:
            headers = {}

        self._get_entry_point()

        uri = join_uri(self._server, uri)

        if DEBUG:
            print(uri)

        response = None

        if method is 'PUT':
            response = self._http.put(uri, headers=headers, data=body, **kwargs)
        elif method is 'GET':
            response = self._http.get(uri, headers=headers, params=body, **kwargs)
        elif method is 'POST':
            response = self._http.post(uri, headers=headers, data=body, **kwargs)
        elif method is 'DELETE':
            response = self._http.delete(uri, headers=headers, data=body, **kwargs)
        elif method is 'HEAD':
            response = self._http.head(uri, headers=headers, data=body, **kwargs)
        else:
            print 'unsupported HTTP method'
            return

        if (response is not None and not response.ok) or is_xnat_error(response.content):
            if DEBUG:
                print(response.keys())
                print(response.get("status"))

            catch_error(response.content, '''pyxnat._exec failure:
    URI: {response.url}
    status code: {response.status_code}
    headers: {response.headers}
    content: {response.content}
'''.format(response=response))

        return response.content

    def _get_json(self, uri):
        """ Specific Interface._exec method to retrieve data.
            It forces the data format to csv and then puts it back to a
            json-like format.

            Parameters
            ----------
            uri: string
                URI of the resource to be accessed. e.g. /REST/projects

            Returns
            -------
            List of dicts containing the results
        """
        if 'format=json' in uri:
            uri = uri.replace('format=json', 'format=csv')
        else:
            if '?' in uri:
                uri += '&format=csv'
            else:
                uri += '?format=csv'

        content = self._exec(uri, 'GET')

        if is_xnat_error(content):
            catch_error(content)

        json_content = csv_to_json(content)

        # add the (relative) path field for files
        base_uri = uri.split('?')[0]
        if uri_last(base_uri) == 'files':
            for element in json_content:
                element['path'] = file_path(element['URI'])
        return json_content

    def _get_head(self, uri):
        if DEBUG:
            print('GET HEAD')

        response = self._http.head('{server}{uri}'.format(server=self._server, uri=uri))

        if not response.ok:
            time.sleep(1)
            self._http.head('{server}{uri}'.format(server=self._server, uri=uri))

        return response.headers

    def save_config(self, location):
        """ Saves current configuration - including password - in a file.

            .. warning::
                Since the password is saved as well, make sure the file
                is saved at a safe location with appropriate permissions.

            .. note::
                This method raises NotImplementedError for an anonymous
                interface.

            Parameters
            ----------
            location: string
                Destination config file.
        """
        if self._anonymous:
            raise NotImplementedError(
                'no save_config() for anonymous interfaces')

        if not os.path.exists(os.path.dirname(location)):
            os.makedirs(os.path.dirname(location))

        fp = open(location, 'w')
        config = {'server': self._server,
                  'user': self._user,
                  'password': self._pwd,
                  }
        if self._proxy_url:
            config['proxy'] = self._proxy_url.geturl()

        json.dump(config, fp)
        fp.close()

    def load_config(self, location):
        """ Loads a configuration file and replaces current connection
            parameters.

            .. note::
                This method raises NotImplementedError for an anonymous
                interface.

            Parameters
            ----------
            location: string
                Configuration file path.
        """
        if self._anonymous:
            raise NotImplementedError(
                'no load_config() for anonymous interfaces')

        if os.path.exists(location):
            fp = open(location, 'rb')
            config = json.load(fp)
            fp.close()

            self._server = str(config['server'])
            self._user = str(config['user'])
            self._pwd = str(config['password'])

            if 'proxy' in config:
                self.__set_proxy(str(config['proxy']))
            else:
                self.__set_proxy(None)

        else:
            raise Exception('Configuration file does not exist.')

    def version(self):
        return self._exec('/data/version')

    def set_logging(self, level=0):
        pass

    def disconnect(self):
        """
            Tell XNAT to disconnect this session
        """
        self._exec('/data/JSESSION', method='DELETE')
        pass

    def get(self, uri, **kwargs):
        '''
        Wrapper around requests.get()
        returns rquests.response object
        '''
        uri = join_uri(self._server, uri)
        response = self._http.get(uri, **kwargs)
        return response

    def put(self, uri, **kwargs):
        '''
        Wrapper around requests.put()
        returns rquests.response object
        '''
        uri = join_uri(self._server, uri)
        response = self._http.put(uri, **kwargs)
        return response

    def post(self, uri, **kwargs):
        '''
        Wrapper around requests.post()
        returns rquests.response object
        '''
        uri = join_uri(self._server, uri)
        response = self._http.post(uri, **kwargs)
        return response

    def delete(self, uri, **kwargs):
        '''
        Wrapper around requests.delete()
        returns rquests.response object
        '''
        uri = join_uri(self._server, uri)
        response = self._http.delete(uri, **kwargs)
        return response

    def head(self, uri, **kwargs):
        '''
        Wrapper around requests.head()
        returns rquests.response object
        '''
        uri = join_uri(self._server, uri)
        response = self._http.head(uri, **kwargs)
        return response
