import pandas as pd
import numpy as np
import json
import requests
import re
import math 
import uuid

class Response:
    """
    The Response class holds the response payload from an OptlyAPI class instance request

    Parameters:
    -----------
    response: obj
        Response of a request from a instance of the OptlyAPI class

    accountId: int (optional)
        Optimizely account ID, used for sending events via the Event API

    """
    def __init__(self, response, accountId=False):
        """
        Please refer to help(Response) for more infomration
        """
        self.result = response 
        self.accountId = accountId

    def filter(self, filter_dict):
        """
        The filter method filters the response from the client based on the filter_dict

        Parameters:
        -----------
        filter_dict: (dict
            Dictionary to filter API response
            example: {'audience_conditions': 'everyone', 'status':'paused'} --> grab only experiments with these conditions
        """
        self.result = [x for x in self.result if all(str(x[y]) == z or (hasattr(x[y], "__iter__") and (z in str(x[y]) or any(z in str(d.values) for d in x[y] if isinstance(d, dict)))) for y,z in filter_dict.items())] 

        return self


    def construct_payload(self, column_mapping={}, client_name='optly_api', client_version='0.1', enrich_decisions=True, send_events=False, convert_to_timestamps=False, generate_uuids=False):
        """
        The construct_payload method takes user event data and constructs a JSON object that follows the Optimizely Event API schema
        https://optimizely.github.io/docs/api/event/v1/

        Parameters:
        -----------

        column_mapping: dict
            Dictionary of column mappings that correspond to the predefined Event API schema (see above reference)

        client_name: str (default value: 'optly_api')
            Name of client that sends off the events via the Event API
            
        client_version: str (default value: '0.1')
            Version of client 

        send_events: bool (default value: False)
            Flag to denote whether to send off the json payload returned by this method to Optimizely's events endpoint

        convert_to_timestamps: bool (default value: False)
            Flag to denote whether to convert timestamp column of type datetime to timestamps of milliseconds

        generate_uuids: bool (default value: False)
            Flag to denote whether to generate uuids for each event sent

        """

        values = ['visitor_id', 'tags', 'entity_id', 'type', 'timestamp', 'revenue', 'value', 'uuid', 'attributes']

        # Convert the columns of the ingested CSV/TSV to Optimizely recognized headers
        if column_mapping:
            initial_mapping = dict(zip(values, values))
            for i,j in column_mapping.items():
                initial_mapping[i] = j
            final_mapping = {v: k for k, v in initial_mapping.items()}
            self.result = self.result.rename(columns=final_mapping)
        
        if convert_to_timestamps: # Convert datetime to timestamps in milleseconds
            self.result['timestamp'] = pd.to_datetime(self.result['timestamp']).values.astype(np.int64) / 1000

        # Constructing event payload
        event_payload = {
                            "account_id": str(self.accountId),
                            "visitors":[],
                            "anonymize_ip": True, 
                            "client_name": str(client_name), 
                            "client_version": str(client_version), 
                            "enrich_decisions": enrich_decisions
                        }

        if 'attributes' in self.result.columns and not self.result['attributes'].isnull().all():
            tuples_list = list(zip(self.result['visitor_id'], self.result['attributes']))
            unique_tuples = [t for t in (set(tuple(i) for i in tuples_list))] # unique tuples of visitor_id and attribute value
            for x in unique_tuples:
                event_payload['visitors'].append(self._construct_visitor_obj(x[0], self.result.loc[(self.result['visitor_id'] == x[0]) & (self.result['attributes'] == x[1])], attributes=x[1], generate_uuids=generate_uuids))
        else:
            unique_visitors = self.result['visitor_id'].unique() # unique Optimizely visitor ID's 

            for x in unique_visitors:
                event_payload['visitors'].append(self._construct_visitor_obj(x, self.result.loc[self.result['visitor_id'] == x], generate_uuids=generate_uuids))

        json_payload = json.dumps(event_payload)

        status_code = "Payload not sent"

        if send_events:
            r = requests.post('https://logx.optimizely.com/v1/events', data = json_payload)
            status_code = "Status Code: {}".format(r.status_code)
            
        return (status_code,json_payload)


    def list_ids (self, key):

        """
        This method returns a Response instance that contains a list of unique Optimizely ID's that are found in an array of Optimizely assets

        Parameters:
        -----------

        key: str 
        The key in the Optimizely asset object to find unique ids (e.g page_ids, audience_conditions etc.)
        """

        list_of_key_values = [str(x[key]) for x in self.result]

        self.result = list(dict.fromkeys([re.findall(r'\b\d+\b', x)[0] for x in list_of_key_values if len(re.findall(r'\b\d+\b', x)) !=0]))

        return self 
        


    def _construct_visitor_obj(self, visitor_id, df_slice, generate_uuids=False, attributes=False):

        """
        Internal method, only to be used by the class itself. NOT FOR PUBLIC USE
        """
    
        visitor_obj = {"visitor_id":visitor_id, "attributes":json.loads(attributes)} if attributes else {"visitor_id":visitor_id}
        
        events_array = []
        sig_types = ['revenue', 'value', 'tags', 'entity_id', 'timestamp']

        for _, row in df_slice.iterrows():
            args = {}
            for s in sig_types:
                val = False
                if s in row and not pd.isnull(row[s]):
                    val = row[s]
                args[s] = val
            if generate_uuids:
                args['uuid'] = uuid.uuid1()
            else:
                args['uuid'] = row['uuid']
            events_array.append(getattr(self, '_construct_event_obj')(**args))
                

        visitor_obj["snapshots"] = [{"decisions":[], "events":events_array}]

        return visitor_obj

    
    def _construct_event_obj(self,entity_id, timestamp, uuid, value, tags, revenue):

        """
        Internal method, only to be used by the class itself. NOT FOR PUBLIC USE
        """

        event_obj = {
                        "entity_id": str(entity_id), 
                        "timestamp": int(timestamp), 
                        "uuid": str(uuid), 
                    }
        if revenue is not False: 
            event_obj['revenue'] = float(revenue) 
        if value is not False: 
            event_obj['value'] = int(value)
        if tags is not False:
            event_obj['tags'] = json.loads(tags)
        
        return event_obj