import requests
import base64
import json
import numpy as np
import os
import pandas as pd

class Ramiel():
    def __init__(self, **authenticate_creds):
        self.filepassword = authenticate_creds['filepassword']
        self.username = authenticate_creds['username']
        self.b64password = authenticate_creds['b64password']
        self.https = authenticate_creds['https']
        self.path = authenticate_creds['path']
        self.port = authenticate_creds['port']

        ## Second verification of username and password
        verify_URL = 'https' + '://' + self.path + ':' + self.port + '/cloud/verifyUser' if self.https else 'http' + '://' + self.path + ':' + self.port + '/cloud/verifyUser'
        resp_verify = requests.get(verify_URL,auth=(self.username, self.b64password))
        if resp_verify.status_code != 200:
            raise AttributeError("Username-Password combination not recognized. Please try again.")

    def fit(self, * , X='', DataUploaded = True,FileName = '', specs={},
            instanceName = '',folderName = '', specFileName = '',verbose=True, Pivots = 256,
            Probability = .95, AcceptedError = 1.2, K = 10,
            Storage = 1, Parallelism = 2, PivotSampleSize = 20000, CacheSize = 1000000, 
            IndexCount = 3, MaximumBytesPerObject = 500000, IndexSampleSize = 100):

        """
        Train a Ramiel instance
        
        Parameters
        ----------
        X: array or pandas DataFrame (default = ''), shape = (n_samples, n_features). Training data. If X is an array, the first row must be the headers. If X is a pandas DataFrame, the column names will be used as the headers. Optional, only needed if 'DataUploaded' is set to False.\n 
        DataUploaded: boolean (default = True). If training data already uploaded to simMachines's software, set to True. If not, set to False and angels can be trained on new data not yet uploaded to the software by passing variables X and y to the classifier. \n
        FileName: string (default = ''). Name of the data file if new data is being used to train classifier. Optional, not needed if DataUploaded = True. \n
        specs: dictionary (default = {}). Data specification types for training data. Dictionary keys represent the header names, and the values represent the data types. Optional, highly recommended if DataUploaded = False. \n
        instanceName: string (default = ''), Name of instance to be trained.\n
        folderName: string (default = ''), Name of folder containing training data.\n
        specFileName: string (default = ''), Name of spec file to use for training. If empty, the most recent compatible file will be chosen.\n
        verbose: bool (default = True). Enable verbose output\n
        Pivots: int (default = 256), The number of primary search points in the engine. This may improve query speed at the cost of training time. (Range 256 to 1024)\n
        Probability: float (default = .95), Minimum accepted probability that the distance between the result and the query will be within the Accepted Error range.  Any result with lower probability will be discarded.\n
        AcceptedError: float (default = 1.2), Maximum accepted difference in distance between returned objects and the query object (Minimum = 1).\n
        K: int (default = 10), Specify the k number of results for the nearest neighbor search.\n
        Storage: int (default = 1)\n
        Parallelism: int (default = 2)\n
        PivotSampleSize: int (default = 20000)\n
        CacheSize: int (default = 1000000)\n
        IndexCount: int (default = 3)\n
        MaximumBytesPerObject: int (default = 500000)\n
        IndexSampleSize: int (default = 100)        
        """

        ## Building base URL's
        base_cloud_url = 'https' + '://' + self.path + ':' + self.port + '/cloud' if self.https else 'http' + '://' + self.path + ':' + self.port + '/cloud'
        self.base_cloud_url = base_cloud_url
        base_v2_url = 'https' + '://' + self.path + ':' + self.port + '/V2.0' if self.https else 'http' + '://' + self.path + ':' + self.port + '/V2.0'
        self.base_v2_url = base_v2_url
        
        ## Checking if folder exists
        if folderName:
            if instanceName:
                self.instanceName = instanceName
                if DataUploaded:
                    ## Warnings
                    if X:
                        raise Warning("Since 'DataUploaded' = True, 'X' will be ignored.")
                    if FileName:
                        raise Warning("Since 'DataUploaded' = True, 'FileName' will be ignored.")
                    
                    ## getting Specs from folder
                    specs_URL = self.base_cloud_url +'/listSpecByFolder?folderName=' + folderName
                    resp_specs = requests.get(specs_URL,auth=(self.username, self.b64password))
                    if resp_specs.status_code == 200:
                        specs_json = json.loads(resp_specs.content.decode())
                        columns_str = 'COLUMNS='
                        valid_specFileName = 0
                        createdDate = 0
                        for spec_file in specs_json['list']:
                            if 'RAMIEL' in spec_file['angelName']:
                                if specFileName:
                                    if specFileName == spec_file['fileName']:
                                        valid_specFileName = 1
                                        for col in spec_file['specsMap'].keys():
                                            columnName = col
                                            specType = spec_file['specsMap'][col]
                                            columns_str += columnName + ':' + specType + ','
                                else:
                                    ## If specFileName not given by user, choose most 
                                    ## recent compatible file present 
                                    createdDate_curr = spec_file['createdDate']
                                    if createdDate_curr > createdDate:
                                        createdDate = createdDate_curr
                                        for col in spec_file['specsMap'].keys():
                                            columnName = col
                                            specType = spec_file['specsMap'][col]
                                            columns_str += columnName + ':' + specType + ','
                    else:
                        raise AttributeError(resp_specs.content.decode())
                    
                    ## Error message if specFileName not recognized
                    if not valid_specFileName and specFileName:
                        raise AttributeError("Spec file name not recognized. Make sure to include file extension (.json, .tsv, etc.) in specFileName variable")

                ## New data not uploaded yet is being used to train angel
                else:         
                    ## Warnings
                    if not specs:
                        raise Warning("Data type specifications ('specs') not provided. Automated specification types will be used in model creation. However, it is recommended that spec files are created by the user to improve model performence.")
                    
                    if specFileName:
                        raise Warning("A spec file name was provided for data not yet uploaded to aimMachines's software. 'specFileName' will be ignored.")
                    
                    if isinstance(X,pd.DataFrame):
                        HeadersX = list(X.columns)
                        X = np.asarray(X)
                    elif isinstance(X,pd.Series):
                        HeadersX = [X.name]
                        X = np.asarray(X)
                    else:
                        if len(X) > 0:
                            HeadersX = list(X[0])
                            X = np.asarray(X)
                        else:
                            raise AttributeError('X must be provided if DataUploaded == False')
                        
                    if not FileName:
                        raise AttributeError('Missing file name (FileName) for data to be uploaded.')

                    ## Create folder if "folderName" is not already created
                    listFolders_URL = self.base_cloud_url +'/listFolders'
                    resp_folders = requests.get(listFolders_URL,auth=(self.username, self.b64password))
                    if resp_folders.status_code == 200:
                        resp_folders_json = json.loads(resp_folders.content.decode())
                        folder_exists = 0
                        for item in resp_folders_json['list']:
                            if item['name'] == folderName:
                                folder_exists = 1
                    else:
                        raise AttributeError(resp_folders.content.decode())
                            
                    if not folder_exists:
                        createFolder_URL = self.base_cloud_url +'/createFolder'
                        folder_data = {"folderName" : folderName}
                        resp_CreateFolder = requests.post(createFolder_URL,data = folder_data,auth=(self.username, self.b64password))
                        if resp_CreateFolder.status_code == 200:
                            pass
                        else:
                            raise AttributeError(resp_CreateFolder.content.decode())
                    else:
                        raise AttributeError('Folder with name %s already exists.' % folderName)
                        
                    ## Upload data to "folderName"
                    uploadFile_URL = self.base_cloud_url + '/uploadFile'
                    
                    ## Validating shape of input data
                    if X.shape[1] != len(HeadersX):
                        raise ValueError('X.shape[1] must equal the length of HeadersX')

                        
                    with open(FileName,'wb') as r:
                        ## Header
                        header_string = '\t'.join(HeadersX) + '\n'
                        r.write(header_string.encode('utf8'))
                        for i,row in enumerate(X):
                            row_str = '\t'.join([str(val) for val in row.tolist()])
                            if i == (len(X)-1):
                                pass
                            else:
                                row_str += '\n'
                            r.write(row_str.encode('utf8'))
                        r.flush()
                    r.close()
                    
                    filesize = os.path.getsize(FileName)
                    
                    with open(FileName,'rb') as f:
                        file_stream = {'fileData' : f}
                        file_data = {'fileName' : FileName
                                  ,'fileSize' : filesize
                                  ,'folderName' : folderName
                                  ,'authorization' : self.filepassword
                                  }
                        resp_query = requests.post(uploadFile_URL
                                             ,files = file_stream
                                                  ,data = file_data
                                                  ,auth = (self.username, base64.b64decode(self.b64password).decode()))
                    f.close()
                    os.remove(FileName)
                    
                    if not resp_query.status_code == 200:
                        raise AttributeError(resp_query.content.decode())
                    
                    ## Create spec file
                    columns_str = 'COLUMNS='
                    if not specs:
                        # Use recommended specs from /getSpecsOfFolder
                        getSpecs_URL = self.base_cloud_url + '/getSpecsOfFolder?folderName=' + folderName
                        resp_specs = requests.get(getSpecs_URL,auth=(self.username,self.b64password))
                        if resp_specs.status_code == 200:
                            resp_specs_json = json.loads(resp_specs.content.decode())
                            for item in resp_specs_json['columns']:
                                col_name = item['columnName']
                                spec_type = item['typeOfColumn']
                                columns_str += col_name + ':' + spec_type + ','
                        else:
                            raise AttributeError(resp_specs_json.content.decode())
                    else:
                        for item in specs.items():
                            col_name, spec_type = item
                            columns_str += col_name + ':' + spec_type + ','
                            
                    
                columns_str = columns_str[:-1]
                params_str = columns_str + '_@_@_K=' + str(K) + '_@_@_PIVOTS=' + str(Pivots) + '_@_@_PROBABILITY=' + str(Probability) + \
                            '_@_@_ACCEPTED_ERROR=' + str(AcceptedError) + '_@_@_EXECUTE_FOLD=false' + '_@_@_PIVOT_SAMPLE_SIZE=' + \
                            str(PivotSampleSize) + '_@_@_CACHE_SIZE=' + str(CacheSize) + '_@_@_INDEX_COUNT=' + str(IndexCount) + '_@_@_MAXIMUM_BYTES_PER_OBJECT=' + \
                            str(MaximumBytesPerObject) + '_@_@_INDEX_SAMPLE_SIZE=' + str(IndexSampleSize)
                ## Building API call
                instance_data = {"instanceName" : instanceName,
                                 "folderName" : folderName,
                                 "angelName": 'RAMIEL',
                                 "params": params_str,
                                 "storage" : Storage,
                                 "parallelism" : Parallelism,
                                 "authorization" : self.filepassword
                        }
                ## POST request to create instance
                createInstance_url = self.base_cloud_url + '/createInstance'
                resp = requests.post(createInstance_url,data = instance_data, auth = (self.username, self.b64password))
                
                if resp.status_code == 200:
                    listInstances_url = self.base_cloud_url + '/listInstances'
                    status = 'Unknown'
                    while status not in ('RUNNNING', 'BUILD_ERROR'):
                        resp_status = requests.get(listInstances_url, auth=(self.username, self.b64password))
                        if resp_status.status_code == 200:
                            resp_JSON = json.loads(resp_status.content.decode())
                            
                            ## looping through instances
                            for l in resp_JSON['list']:
                                if l['label'] == self.instanceName:
                                    status_curr = l['status']
                                    if status != status_curr:
                                        status = status_curr
                                        if verbose:
                                            print('Status: ' + status)
                                        else:
                                            pass
                        else:
                            raise AttributeError(resp_status.content.decode())
                        if status == 'RUNNING':
                            print("Instance '" + self.instanceName + "' is ready for querying.")
                            break
                        elif status == 'BUILD_ERROR':
                            print("Instance '" + self.instanceName + "' returned a status of '" + status + "'.")
                            break
                else:
                    ## If instance not created, set instance name to '' 
                    self.instanceName = ''
                    raise AttributeError(resp.content.decode())
            else:
                raise AttributeError("Must enter instance name as 'instanceName' variable")
        else:
            raise AttributeError("Must specify name of folder to train Leliel with (folderName variable).")
        
        return self
    
        
    def get_neighbors(self, X, *, version = ''):
        """
        Retrive neighbors using trained Ramiel instance.
        
        Parameters
        ----------
        X: {array-like}. Samples to retrieve neighbors for. shape = (n_samples, n_features). 
        First line must be headers. 
        
        Returns
        ----------
        neighbors_final: list, shape = (n_samples,(K,2))
        ID, distance pairs of nearest neighbors
        """
        if self.instanceName:
            headers = X[0]
            headers_str = '\t'.join(headers)
            samples = X[1:]
            neighbors_final=[]
            if samples.shape[0]> 0:
                failed_queries = 0
                for samp in samples:
                    neighbors=[]
                    sample_str = '\t'.join(samp)
                    queryObject_url = self.base_cloud_url + '/queryObject'
                    if version:
                        query_data = {"instanceName" : self.instanceName,
                                      "version" : version,
                                      "query" : headers_str + '\n' + sample_str,
                                      "authorization" : self.filepassword
                                }
                    else:
                        query_data = {"instanceName" : self.instanceName,
                                      "query" : headers_str + '\n' + sample_str,
                                      "authorization" : self.filepassword
                                }              
                    resp_query = requests.post(queryObject_url,data = query_data, auth = (self.username, self.b64password))
                        
                    if resp_query.status_code == 200:
                        ## Converting response to JSON object
                        resp_query_json = json.loads(resp_query.content.decode())
                        for item in resp_query_json['results']:
                            neighbors.append((item['id'],float(item['distance'])))
                        neighbors_final.append(neighbors)
                    else:
                        failed_queries += 1
                        error_message = resp_query.content.decode()
                
                if not failed_queries:
                    return neighbors_final
                else:
                    raise AttributeError("One or more queries producing errors. API error message: " + error_message + ".")        
            else:
                raise AttributeError("Must contain both headers and query objects for prediction.")           
        else:
            raise AttributeError("Must create instance by using fit() before predicting.")