#!/usr/bin/env python
from requests import Request, Session, utils
from itertools import islice
import mimetypes
from seams.exceptions import SeamsException, SeamsAPIException
import json
import msal
import os

class Seams(object):
    '''
    A Python interface for the Seams API
    '''
    
    def __init__(self, 
                 URL: str):
        '''
        Create an instance of the Seams API interface
        '''

        # Remove any trailing slashe, if any
        self.URL = URL.rstrip('/')
        self.connected = False
        self.schemas = None
        self.current_schema = None


    def connect(self,
                secret: str = None,
                app_id: str = None,
                tenant_id: str = None,
                ad_auth: str =None) -> None:
        '''
        Connect to the Seams API
        :param secret:
            A secret given by RJLG staff to access the Seams SDK  **REQUIRED**
        :param app_id:
            An application ID given by RJLG staff to access the Seams SDK  **REQUIRED**
        :param tenant_id:
            An AD tenant ID given by RJLG staff to access the Seams SDK  **REQUIRED**
        :returns:
            None
        '''
        
        if secret is None:
            secret = os.getenv("AD_APP_SECRET")
        if app_id is None:
            app_id = os.getenv("AD_APP_ID")
        if tenant_id is None:
            tenant_id = os.getenv("AD_TENANT_ID")
        if ad_auth is None:
            ad_auth = os.getenv("AD_AUTH_MODE")

        self.app_id = app_id
        if ad_auth is not None and ad_auth == "local":
            authority = "https://seams-ad.rjlglims.com/adfs"
            self.scopes = ["openid","profile"]
        else:
            authority="https://login.microsoftonline.com/{}".format(tenant_id)
            self.scopes = ["api://{}/.default".format(self.app_id)]

        self.app = msal.ConfidentialClientApplication(
                self.app_id,
                authority=authority,
                client_credential=secret)

        self.aquire_token()
        

    def aquire_token(self) -> None:
        '''
        Calls acquire_token_silent to refresh the existing token if necessary
        '''
        result = None
        result = self.app.acquire_token_silent(scopes=["api://{}/access_as_user".format(self.app_id)], account=None)
        if result is None:
            result = self.app.acquire_token_for_client(scopes=self.scopes)
        if "error" in result:
            print("Error connecting with given credentials")
            raise SeamsException(result["error_description"])
        else:
            bearer = result["access_token"]
            self.connected = True
        return bearer

        self.get_schemas(refresh=True)


    def disconnect(self) -> None:
        '''
        Disconnect from the Seams API 
        '''
        self.connected = False
        self.schemas = None
        self.current_schema = None


    def me(self) -> str:
        '''
        Verifies the user is connected and returns data relating to the graph database

        :returns:
            JSON object representing the graph database
        '''
        
        response = self.__seams_api_request('GET', '/auth/me', schema_api_url=False)
        return response.json()


    def whoami(self) -> str:
        '''
        Gives info on the connection information of the current Seams object

        :returns:
            The bearer token, URL, connected status, and secret
        '''

        try:
            response = {
                'url': self.URL,
                'connected': self.connected
            }
            return response
        except:
            return {'url': self.URL, 'connected': self.connected}


    def get_schemas(self, refresh = False) -> dict:
        '''
        Gets the list of available schemas in the current database

        :param refresh
            Refresh the list of schemas from the database or use the local cached copy
        :returns:
            JSON object representing the schema graph databases
        '''

        if (self.schemas is not None and refresh is False):
            return self.schemas

        schemas = {}

        response = self.me()
        for schema in response["tenants"]:
            name = schema["tenant"]["name"]
            schemas[name] = schema["tenant"]

        self.schemas = schemas

        return schemas


    def get_schema_by_name(self, schema_name:str) -> dict:
        '''
        Get the schema by name

        :param schema_name:
            The name of the schema to get  **REQUIRED**
        :returns:
            A graph dictionary
        '''
        return self.get_schemas()[schema_name]


    def get_schema_by_id(self, schema_id: str) -> dict:
        '''
        Get the current schema for the Id

        :returns:
            The schema matching the Id
        '''
        schemas = self.get_schemas()
        for schema_name, schema_properties in schemas.items():
            if schema_properties["id"] == schema_id:
                return schemas[schema_name]

        return None


    def get_current_schema(self) -> dict:
        '''
        Get the current schema

        :returns:
            The current schema
        '''
        return self.current_schema


    def get_current_schema_id(self) -> dict:
        '''
        Get the current schema Id

        :returns:
            The current schema Id
        '''
        if self.current_schema is None:
            return None
        return self.current_schema["id"]


    def set_current_schema(self, schema: dict) -> None:
        '''
        Set the current schema

        :returns:
            None
        '''
        self.current_schema = schema


    def set_current_schema_by_id(self, schema_id: str) -> None:
        '''
        Set the current schema from the id

        :returns:
            None
        '''
        self.current_schema = self.get_schema_by_id(schema_id) 


    def get_vertex_by_id( self, 
                          vertex_id: str,
                          schema_id = None ) -> str:
        '''
        Get a vertex by vertex id

        :param vertex_id:
            The vertex id of the node the user is getting  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A vertex
        '''

        response = self.__seams_api_request('GET', '/vertex/{}'.format(vertex_id) , schema_id=schema_id)
        if response is None:
            return None

        return response.json()
    

    def get_vertices_by_label(self, 
                              vertex_label: str,
                              schema_id: str = None) -> str:
        '''
        Get all vertices with a specific label

        :param vertex_label:
            The label of the vertex the user is getting  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            JSON formatted list of vertices
        '''

        response = self.__seams_api_request('GET', '/vertices/{}'.format(vertex_label) , schema_id=schema_id)
        if response is None:
            return None

        return response.json()


    def update_vertex(self, 
                      vertex_id: str, 
                      vertex_label: str, 
                      attributes: dict,
                      schema_id: str = None,
                      chunk_attributes: bool = False) -> str:
        '''
        Update a vertex 

        :param vertex_id:
            The vertex id of the node the user is getting  **REQUIRED**
        :param vertex_label:
            The label of the vertex the user is getting  **REQUIRED**
        :param attributes:
            A dictionary of key/value pairs that will represent the data fields of the vertex  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            JSON formatted vertex with the updates
        '''


        response = None
        if not chunk_attributes:
            body = self.__properties_formatter(vertex_label, attributes)
            response = self.__seams_api_request('PUT', '/vertex/update/{}'.format(vertex_id), content='application/json', data=body, schema_id=schema_id)

        # Chunk the attributes
        for attributeChunk in self.__chunk_attributes(attributes, 10):
            body = self.__properties_formatter(vertex_label, attributeChunk)
            response = self.__seams_api_request('PUT', '/vertex/update/{}'.format(vertex_id), content='application/json', data=body, schema_id=schema_id)

        return response.json()


    def create_vertex(self, 
                      vertex_label: str,
                      vertex_name: str, 
                      attributes: dict = None,
                      schema_id: str = None,
                      chunk_attributes: bool = False ) -> str:
        '''
        Create a vertex

        :param vertex_label:
            The label of the vertex the user is creating  **REQUIRED**
        :param vertex_name:
            The name of the vertex the user is creating **REQUIRED**
        :param attributes:
            A dictionary of key/value pairs that will represent the data fields of the vertex  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A JSON formatted object representing the new vertex
        '''
        body = {}

        if not chunk_attributes:
            # Create the vertex with no chunking
            body = self.__properties_formatter(vertex_label, attributes, vertex_name=vertex_name)
            response = self.__seams_api_request('POST', '/vertex/create', content='application/json', data=body, schema_id=schema_id)
            return response.json()

        # Chunk the attributes
        if attributes:
            body = self.__properties_formatter(vertex_label, {"name":vertex_name}, vertex_name=vertex_name)

        # Create the initial vertex
        response = self.__seams_api_request('POST', '/vertex/create', content='application/json', data=body, schema_id=schema_id)

        # Chunk the properties as updates
        for attributeChunk in self.__chunk_attributes(attributes, 10):
            body = self.__properties_formatter(vertex_label, attributeChunk)
            response = self.__seams_api_request('PUT', '/vertex/update/{}'.format(response.json()['id']), content='application/json', data=body, schema_id=schema_id)

        return response.json()


    def upsert_vertex(self, 
                      vertex_label: str,
                      vertex_name: str, 
                      attributes:dict = None,
                      schema_id:str = None,
                      chunk_attributes: bool = False) -> dict:
        '''
        Create a vertex

        :param vertex_label:
            The label of the vertex the user is creating  **REQUIRED**
        :param vertex_name:
            The name of the vertex the user is creating **REQUIRED**
        :param attributes:
            A dictionary of key/value pairs that will represent the data fields of the vertex  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A JSON formatted object representing the new vertex
        '''

        # Find an existing vertex with the label and exact name
        url_string = '/vertex?filter%5Blabel%5D={}&filter%5Bproperty%5D=name%3D{}&filter%5Bunique%5D=&orderBy=&limit=1&offset='.format(utils.quote(vertex_label), utils.quote(vertex_name))
        response = self.__seams_api_request('GET', url_string, schema_id=schema_id)
        existingVertex = None

        for vertex in response.json()["vertices"]:
            if vertex["name"] == vertex_name:
                existingVertex = vertex
        
        response = None
        if ( existingVertex ):
            vertex_id = existingVertex["id"]
            response = self.update_vertex(vertex_id, vertex_label, attributes, schema_id=schema_id, chunk_attributes=chunk_attributes)
        else:
            response = self.create_vertex(vertex_label, vertex_name, attributes=attributes, schema_id=schema_id, chunk_attributes=chunk_attributes)

        return response


    def delete_vertex(self, 
                      vertex_id: str,
                      schema_id: str = None) -> str:
        '''
        Delete a vertex

        :param vertex_id:
            The vertex id of the node the user is getting  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A message specifying if the delete was successful or not
        '''

        response = self.__seams_api_request('DELETE', '/vertex/delete/{}'.format(vertex_id), schema_id=schema_id)
        return response.text
        

    def get_edge_vertices(self, 
                            vertex_id: str, 
                            other_vertex_label: str, 
                            direction: str,
                            schema_id: str = None) -> str:
        '''
        Retreive all edges on a vertex based on direction

        :param vertex_id:
            The vertex id of the node the user is getting  **REQUIRED**
        :param other_vertex_label:
            The label of the vertex for the OTHER vertex (used to combine the vertex labels to create edge name)  **REQUIRED**
        :param direction:
            The direction of the edge  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A JSON formatted list of all edges on a vertex
        '''

        response = self.__seams_api_request('GET', '/edgeVertices/{}/{}/{}'.format(vertex_id, other_vertex_label, direction), schema_id=schema_id)
        if response is None:
            return None
        return response.json()
        

    def attach_edges(self, 
                    parent_id: str, 
                    child_vertices: list,
                    schema_id: str = None) -> str:
        '''
        Attach edge from one vertex to another

        :param parent_id:
            The vertex id of the parent vertex  **REQUIRED**
        :param child_vertices:
            A list of vertex id's to attach the edge to  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A success or fail message if the edges were attached
        '''
        body = {
            'parentVertex': parent_id,
            'edgeVertices': child_vertices
        }
        response = self.__seams_api_request('POST', '/edge/attach', json=body, schema_id=schema_id)

        return response.text
        

    def attach_label_to_edge(self, 
                             parent_label: str, 
                             edge_name: str, 
                             child_id: str,
                             schema_id: str = None) -> str:
        '''
        Attach label to an edge

        :param parent_label:
            The label of the parent vertex  **REQUIRED**
        :param edge_name:
            The name of the edge  **REQUIRED**
        :param child_id:
            A single vertex id of the child  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A success or fail message if the label was attached
        '''

        body = '{{"parentVertexLabel": "{}", "edgeName": "{}", "childVertex": "{}"}}'.format(parent_label, edge_name, child_id)
        response = self.__seams_api_request('POST', '/edge/attach/label/to', data=body, schema_id=schema_id)
        if response is None:
            return None
        return response.json()


    def attach_label_from_edge(self, 
                               parent_vertex: str, 
                               edge_name: str, 
                               child_label: str,
                               schema_id:str = None) -> str:
        '''
        Attach label from an edge

        :param parent_vertex:
            The parent vertex  **REQUIRED**
        :param edge_name:
            The name of the edge  **REQUIRED**
        :param child_label:
            The label of the child  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A success or fail message if the label was attached
        '''

        body = '{{"parentVertex": "{}", "edgeName": "{}", "childVertexLabel": "{}"}}'.format(parent_vertex, edge_name, child_label)
        response = self.__seams_api_request('POST', '/edge/attach/label/from', data=body, schema_id=schema_id)
        if response is None:
            return None

        return response.json()


    def upload_file(self, 
                    caption: str,
                    filename: str, 
                    file_type: str = 'File',
                    schema_id: str = None) -> str:
        '''
        Upload a sinlge file

        :param filename:
            Filename the user would like to upload  **REQUIRED**
        :param file_type:
            Can be 'File' or 'Image' - defaults to 'File'
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A list of vertex id's for the uploaded files
        '''

        if not os.path.exists(filename):
            raise SeamsAPIException("File upload failed, file not found: {}".format(filename))

        body = {'upload_preset': filename, 'fileType': file_type, 'caption': caption}
        mimetype = mimetypes.guess_type(filename)[0]
        if mimetype == "application/vnd.ms-excel":
            mimetype = "text/csv"

        files = {'file': (filename, open(filename, 'rb'), mimetype)}
        response = None
        response = self.__seams_api_request('POST', '/upload/file', files=files, data=body, schema_id=schema_id)

        return response.json()



    def download_files(self, 
                       vertex_ids: list,
                       schema_id: str = None) -> dict:
        '''
        Bulk download files

        :param *files:
            List of vertex id's the user would like to download  **REQUIRED**
        :param schema_id:
            The id of the SEAMS schema to access  **OPTIONAL**
        :returns:
            A dictionary where the key is the filename and the value is the file contents
        '''

        download_list = {}
        for vertex_id in vertex_ids:
            response = self.__seams_api_request('GET', '/download/file/{}'.format(vertex_id), schema_id=schema_id)
            download_list[self.__file_name_formatter(response.headers['filename'])] = response.text

        return download_list


    def __file_name_formatter(self, 
                              file_name):
        '''
        Private helper function that formats the filename and allows the use of '-' in filenames
        '''

        file_name_list = file_name.split('-')
        del file_name_list[0:5]
        if len(file_name_list) > 1:
            new_file = ''
            for item in file_name_list:
                new_file = new_file + '-' + item
            file_name_list[0] = new_file[1:]
        return file_name_list[0]


    def __properties_formatter(self, 
                               vertex_label: str, 
                               args,
                               vertex_name = None) -> str:
        '''
        Private helper function that formats a list of key value pairs for properties on a vertex
        '''
        for item in args:
            if item != 'status':
                if isinstance(args[item], list):
                    args[item] = json.dumps(args[item])

        if vertex_name:
            return '{{"vertexLabel": "{}", "vertexName": "{}", "properties": {}}}'.format(vertex_label, vertex_name, json.dumps(args)).replace("'", "")
        else:
            return '{{"vertexLabel": "{}", "properties": {}}}'.format(vertex_label, json.dumps(args)).replace("'", "")


    def __seams_api_request(self, 
                       req_type: str, 
                       url:str, 
                       data: dict = None, 
                       content:dict = None, 
                       files = None,
                       json = None,
                       schema_id:str = None,
                       schema_api_url = True):
        '''
        Private helper function that makes a specific type of HTTP request to SEAMS
        '''
        
        bearer = self.aquire_token()
        header = {'Authorization': 'Bearer {}'.format(bearer)}
        if content:
            header['Content-Type'] = content
        session = Session()

        if schema_api_url:
            if schema_id is None:
                schema_id = self.get_current_schema_id()
                if schema_id is None:
                    raise SeamsAPIException("Error: You must provide a schema id for this API call.  There is no current schema Id set or sent.")
            api_url = '{}/tenant/{}{}'.format(self.URL, schema_id, url)
        else:
            api_url = '{}{}'.format(self.URL, url)


        request = Request(req_type, api_url, headers=header, data=data, files=files, json=json)
        prepared = session.prepare_request(request)
        response = session.send(prepared)

        # Raise any HTTP response errors (if any)
        if response.status_code >= 400:
            raise SeamsAPIException("Error {} {}".format(response.status_code, response.text) )
        #response.raise_for_status()

        # Otherwise, return the results
        return response
    

    def __chunk_attributes(self, data, SIZE):
        it = iter(data)
        for i in range(0, len(data), SIZE):
            yield {k:data[k] for k in islice(it, SIZE)}

