from notebook.utils import url_path_join
from notebook.base.handlers import IPythonHandler
from pybis import Openbis
import numpy as np

import os
from urllib.parse import unquote
import yaml

openbis_connections = {}


def _jupyter_server_extension_paths():
    return [{'module': 'jupyter-openbis-extension.server'}]

def _load_configuration(paths, filename='openbis-connections.yaml'):

    if paths is None:
        paths = []
        home = os.path.expanduser("~")
        paths.append(os.path.join(home, '.jupyter'))

    # look in all config file paths of jupyter
    # for openbis connection files and load them
    connections = []
    for path in paths:
        abs_filename = os.path.join(path, filename)
        if os.path.isfile(abs_filename):
            with open(abs_filename, 'r') as stream:
                try:
                    config = yaml.safe_load(stream)
                    for connection in config['connections']:
                        connections.append(connection)
                except yaml.YAMLexception as exc:
                    print(exc)
                    return None

    return connections
        

def load_jupyter_server_extension(nb_server_app):
    """Call when the extension is loaded.
    :param nb_server_app: Handle to the Notebook webserver instance.
    """

    # load the configuration file
    # and register the openBIS connections.
    # If username and password is available, try to connect to the server
    connections = _load_configuration(
        paths    = nb_server_app.config_file_paths,
        filename = 'openbis-connections.yaml'
    )

    for connection_info in connections:
        conn = register_connection(connection_info)
        print("Registered: {}".format(conn.url))
        if conn.username and conn.password:
            try:
                conn.login()
                print("Successfully connected to: {}".format(conn.url))
            except ValueError:
                print("Incorrect username or password for: {}".format(conn.url))
            except Exception:
                print("Cannot establish connection to: {}".format(conn.url))

    # Add URL handlers to our web_app
    # see Tornado documentation: https://www.tornadoweb.org
    web_app = nb_server_app.web_app
    host_pattern = '.*$'
    base_url = web_app.settings['base_url']

    # DataSet download
    web_app.add_handlers(
        host_pattern, 
        [(url_path_join(
            base_url,
            '/openbis/dataset/(?P<connection_name>.*)?/(?P<permId>.*)?/(?P<downloadPath>.*)'), 
            DataSetDownloadHandler
        )]
    )

    # DataSet upload
    web_app.add_handlers( host_pattern, [(
            url_path_join(
                base_url,
                '/openbis/dataset/(?P<connection_name>.*)'
            ),
            DataSetUploadHandler
        )]
    )

    # DataSet-Types
    web_app.add_handlers(
        host_pattern,
        [(
            url_path_join(
                base_url,
                '/openbis/datasetTypes/(?P<connection_name>.*)'
            ),
            DataSetTypesHandler
        )]
    )

    # DataSets for Sample identifier/permId
    web_app.add_handlers(
        host_pattern, 
        [(  
            url_path_join(
                base_url,
                '/openbis/sample/(?P<connection_name>.*)?/(?P<permId>.*)'
            ), 
            SampleHandler
        )]
    )

    # OpenBIS connections
    web_app.add_handlers(
        host_pattern,
        [(
            url_path_join(
                base_url,
                '/openbis/conns'
            ),
            OpenBISConnections
        )]
    )

    # Modify / reconnect to a connection
    web_app.add_handlers(
        host_pattern, 
        [(
            url_path_join(
                    base_url,
                    '/openbis/conn/(?P<connection_name>.*)'
            ),
            OpenBISConnectionHandler
        )]
    )

    print("pybis loaded: {}".format(Openbis))


def register_connection(connection_info):

    conn = OpenBISConnection(
        name                = connection_info.get('name'),
        url                 = connection_info.get('url'),
        verify_certificates = connection_info.get('verify_certificates', False),
        username            = connection_info.get('username'),
        password            = connection_info.get('password'),
        status              = 'not connected',
    )
    openbis_connections[conn.name] = conn
    return conn


class OpenBISConnection:
    """register an openBIS connection
    """

    def __init__(self, **kwargs):
        for needed_key in ['name', 'url']:
            if needed_key not in kwargs:
                raise KeyError("{} is missing".format(needed_key))

        for key in kwargs:
            setattr(self, key, kwargs[key])

        openbis = Openbis(
            url = self.url,
            verify_certificates = self.verify_certificates
        )
        self.openbis = openbis
        self.status = "not connected"

    def is_session_active(self):
        return self.openbis.is_session_active()

    def check_status(self):
        if self.openbis.is_session_active():
            self.status = "connected"
        else:
            self.status = "not connected"

    def login(self, username=None, password=None):
        if username is None:
            username=self.username
        if password is None:
            password=self.password
        self.openbis.login(
            username = username,
            password = password
        )
        # store username and password in memory
        self.username = username
        self.password = password
        self.status  = 'connected'

    def get_info(self):
        return {
            'name'    : self.name,
            'url'     : self.url,
            'status'  : self.status,
            'username': self.username,
            'password': self.password,
        }

class OpenBISConnections(IPythonHandler):

    def post(self):
        """create a new connection

        :return: a new connection object
        """
        data = self.get_json_body()
        conn = register_connection(data)
        if conn.username and conn.password:
            try:
                conn.login()
            except Exception:
                pass
        self.get()
        return

    def get(self):
        """returns all available openBIS connections
        """

        connections= []
        for conn in openbis_connections.values():
            conn.check_status()
            connections.append(conn.get_info())

        self.write({
            'status'     : 200,
            'connections': connections,
            'cwd'        : os.getcwd()
        })
        return


class OpenBISConnectionHandler(IPythonHandler):
    """Handle the requests to /openbis/conn
    """

    def put(self, connection_name):
        """reconnect to a current connection
        :return: an updated connection object
        """
        data = self.get_json_body()

        try:
            conn = openbis_connections[connection_name]
        except KeyError:
            self.set_status(404)
            self.write({
                "reason" : 'No such connection: {}'.format(data)
            })
            return

        try:
            conn.login(data.get('username'), data.get('password'))
        except ConnectionError:
            self.set_status(500)
            self.write({
                "reason": "Could not establish connection to {}".format(connection_name)
            })
            return
        except ValueError:
            self.set_status(401)
            self.write({
                "reason": "Incorrect username or password for {}".format(connection_name)
            })
            return

        self.write({
            'status'     : 200,
            'connection': conn.get_info(),
            'cwd'        : os.getcwd()
        })

    def get(self, connection_name):
        """returns  information about a connection name
        """

        try:
            conn = openbis_connections[connection_name]
        except KeyError:
            self.set_status(404)
            self.write({
                "reason" : 'No such connection: {}'.format(data)
            })
            return

        conn.check_status()

        self.write({
            'status'     : 200,
            'connection': conn.get_info(),
            'cwd'        : os.getcwd()
        })
        return


class SampleHandler(IPythonHandler):
    """Handle the requests for /openbis/sample/connection/permId"""

    def get_datasets(self, conn, permId):
        if not conn.is_session_active():
            try:
                conn.login()
            except Exception as exc:
                self.write({
                    "reason" : 'connection to {} could not be established: {}'.format(conn.name, exc)
                })

        sample = None
        try:
            sample = conn.openbis.get_sample(permId)
        except Exception as exc:
            self.set_status(404)
            self.write({
                "reason" : 'No such sample: {}'.format(permId)
            })
        if sample is None:
            return

        datasets = sample.get_datasets().df
        datasets.replace({np.nan:None}, inplace=True)  # replace NaN with None, otherwise we cannot convert it correctly
        return datasets.to_dict(orient='records')   # is too stupid to handle NaN


    def get(self, **params):
        """Handle a request to /openbis/sample/connection_name/permId
        download the data and return a message
        """
        try:
            conn = openbis_connections[params['connection_name']]
        except KeyError:
            self.write({
                "reason" : 'connection {} was not found'.format(
                    params['connection_name']
                )
            })
            return
        
        datasets = self.get_datasets(conn, params['permId'])
        if datasets is not None:
            self.set_status(200)
            self.write({
                "dataSets": datasets
            })



class DataSetDownloadHandler(IPythonHandler):
    """Handle the requests for /openbis/dataset/connection/permId"""


    def download_data(self, conn, permId, downloadPath=None):
        if not conn.is_session_active():
            try:
                conn.login()
            except Exception as exc:
                self.set_status(500)
                self.write({
                    "reason" : 'connection to {} could not be established: {}'.format(conn.name, exc)
                })
                return

        try:
            dataset = conn.openbis.get_dataset(permId)
        except Exception as exc:
            self.set_status(404)
            self.write({
                "reason" : 'No such dataSet found: {}'.format(permId)
            })
            return

        # dataset was found, download the data to the disk
        try: 
            destination = dataset.download(destination=downloadPath)
        except Exception as exc:
            self.set_status(500)
            self.write({
                "reason": 'Data for DataSet {} could not be downloaded: {}'.format(permId, exc)
            })
            return
            
        # return success message
        path = os.path.join(downloadPath, dataset.permId)
        self.write({
            'url'       : conn.url,
            'permId'    : dataset.permId,
            'path'      : path,
            'dataStore' : dataset.dataStore,
            'location'  : dataset.physicalData.location,
            'size'      : dataset.physicalData.size,
            'statusText': 'Data for DataSet {} was successfully downloaded to: {}.'.format(dataset.permId, path)
        })

    def get(self, **params):
        """Handle a request to /openbis/dataset/connection_name/permId
        download the data and return a message
        """

        try:
            conn = openbis_connections[params['connection_name']]
        except KeyError:
            self.set_status(404)
            self.write({
                "reason":'connection {} was not found'.format(params['connection_name'])
            })
            return
        
        results = self.download_data(conn=conn, permId=params['permId'], downloadPath=params['downloadPath'])


class DataSetTypesHandler(IPythonHandler):
    def get(self, **params):
        """Handle a request to /openbis/datasetTypes/connection_name
        """

        try:
            conn = openbis_connections[params['connection_name']]
        except KeyError:
            self.set_status(404)
            self.write({
                "reason":'connection {} was not found'.format(params['connection_name'])
            })
            return

        try:
            dataset_types = conn.openbis.get_dataset_types()
            dts = dataset_types.df.to_dict(orient='records')

            # get property assignments for every dataset-type
            # and add it to the dataset collection
            for dt in dts:
                dataset_type = conn.openbis.get_dataset_type(dt['code'])
                pa = dataset_type.get_propertyAssignments()
                pa_dict = pa.to_dict(orient='records')
                dt['propertyAssignments'] = pa_dict

            self.write({
                "dataSetTypes": dts
            })
            return

        except Exception as e:
            self.set_status(500)
            self.write({
                "reason":'Could not fetch dataset-types: {}'.format(e)
            })
            return


class DataSetUploadHandler(IPythonHandler):
    """Handle the requests for /openbis/dataset/connection"""

    def upload_data(self, conn, data):
        if not conn.is_session_active():
            try:
                conn.login()
            except Exception as exc:
                self.write({
                    "reason": 'connection to {} could not be established: {}'.format(conn.name, exc)
                })
                return

        try:
            sample = conn.openbis.get_sample(data.get('sampleIdentifier'))
        except Exception as exc:
            self.set_status(404)
            self.write({
                "reason" : 'No such sample: {}'.format(data.get('sampleIdentifier'))
            })
            return

        filenames = []
        for filename in data.get('files'):
            filename = unquote(filename)
            filenames.append(filename)

        try: 
            ds = conn.openbis.new_dataset(
                name        = data.get('name'),
                description = data.get('description'),
                type        = data.get('type'),
                sample      = sample,
                files       = filenames
            ) 
        except Exception as exc:
            self.write({
                "reason": 'Error while creating the dataset: {}'.format(exc)
            })
            return

        try:
            ds.save()
        except Exception as exc:
            self.write({
                "reason": 'Error while saving the dataset: {}'.format(exc)
            })
            return
        
            
        # return success message
        self.write({
            'status': 200,
            'statusText': 'Jupyter Notebook was successfully uploaded to: {} with permId: {}'.format(conn.name, ds.permId)
        })

    def post(self, **params):
        """Handle a request to /openbis/dataset/connection_name/permId
        download the data and return a message
        """

        try:
            conn = openbis_connections[params['connection_name']]
        except KeyError:
            self.write({
                "reason": 'connection {} was not found'.format(params['connection_name'])
            })
            return

        data = self.get_json_body()
        results = self.upload_data(conn=conn,data=data)