import sys
import signal
import requests
from datetime import datetime
from cachetools import TTLCache
from functools import wraps
from threading import Event, Thread
from django.http import JsonResponse


KABLE_ENVIRONMENT_HEADER_KEY = 'KABLE-ENVIRONMENT'
KABLE_CLIENT_ID_HEADER_KEY = 'KABLE-CLIENT-ID'
KABLE_CLIENT_SECRET_HEADER_KEY = 'KABLE-CLIENT-SECRET'
X_CLIENT_ID_HEADER_KEY = 'X-CLIENT-ID'
X_API_KEY_HEADER_KEY = 'X-API-KEY'


class Kable:

    def __init__(self, config):

        print("Initializing Kable")

        if config is None:
            raise RuntimeError(
                "Failed to initialize Kable: config not provided")

        if "environment" not in config:
            raise RuntimeError(
                'Failed to initialize Kable: environment not provided')

        if "client_id" not in config:
            raise RuntimeError(
                'Failed to initialize Kable: client_id not provided')

        if "client_secret" not in config:
            raise RuntimeError(
                'Failed to initialize Kable: client_secret not provided')

        if "base_url" not in config:
            raise RuntimeError(
                'Failed to initialize Kable: base_url not provided')

        self.environment = config["environment"]
        self.kableClientId = config["client_id"]
        self.kableClientSecret = config["client_secret"]
        self.baseUrl = config["base_url"]

        if "debug" in config:
            self.debug = config["debug"]
            if self.debug:
                print("Starting Kable with debug enabled")
        else:
            self.debug = False

        if "max_queue_size" in config:
            self.maxQueueSize = config["max_queue_size"]
            if self.maxQueueSize > 100:
                self.maxQueueSize = 100
        else:
            self.maxQueueSize = 10  # flush after 10 requests queued

        if "disable_cache" in config:
            disableCache = config["disable_cache"]
            if disableCache:
                self.maxQueueSize = 1
        print("Starting Kable with max_queue_size " + str(self.maxQueueSize))

        if "record_authentication" in config:
            self.recordAuthentication = config["record_authentication"]
            if self.recordAuthentication is False:
                print(
                    "Starting Kable with record_authentication disabled, authentication requests will not be recorded")
        else:
            self.recordAuthentication = True

        self.queueFlushInterval = 10  # 10 seconds
        self.queueFlushTimer = None
        self.queue = []

        self.validCache = TTLCache(maxsize=1000, ttl=30)
        self.invalidCache = TTLCache(maxsize=1000, ttl=30)

        url = f"{self.baseUrl}/api/authenticate"
        headers = {
            KABLE_ENVIRONMENT_HEADER_KEY: self.environment,
            KABLE_CLIENT_ID_HEADER_KEY: self.kableClientId,
            X_CLIENT_ID_HEADER_KEY: self.kableClientId,
            X_API_KEY_HEADER_KEY: self.kableClientSecret,
            KABLE_CLIENT_SECRET_HEADER_KEY: self.kableClientSecret,
        }

        try:
            response = requests.post(url=url, headers=headers)
            status = response.status_code
            if (status == 200):
                self.startFlushQueueOnTimer()

                try:
                    signal.signal(signal.SIGINT, self.exitGracefully)
                    signal.signal(signal.SIGTERM, self.exitGracefully)
                except:
                    print("")

                print("Kable initialized successfully")

            elif status == 401:
                print("Failed to initialize Kable: Unauthorized")

            else:
                print(
                    f"Failed to initialize Kable: Something went wrong [{status}]")

        except Exception as e:
            print("Failed to initialize Kable: Something went wrong")

    def record(self, data):
        if (self.debug):
            print("Received data to record")

        clientId = None
        if 'clientId' in data:
            clientId = data['clientId']
            del data['clientId']

        customerId = None
        if 'customerId' in data:
            customerId = data['customerId']
            del data['customerId']

        self.enqueueEvent(clientId=clientId, customerId=customerId, data=data)

    def authenticate(self, api):
        @wraps(api)
        def decoratedApi(*args, **kwargs):
            if self.debug:
                print("Received request to authenticate")

            request = args[0]
            headers = request.headers

            clientId = headers[X_CLIENT_ID_HEADER_KEY] if X_CLIENT_ID_HEADER_KEY in headers else None
            secretKey = headers[X_API_KEY_HEADER_KEY] if X_API_KEY_HEADER_KEY in headers else None

            if self.environment is None or self.kableClientId is None:
                return JsonResponse({"message": "Unauthorized. Failed to initialize Kable: Configuration invalid"}, status=500)

            if clientId is None or secretKey is None:
                return JsonResponse({"message": "Unauthorized"}, status=401)

            if secretKey in self.validCache:
                if self.validCache[secretKey] == clientId:
                    if self.debug:
                        print("Valid Cache Hit")
                    if self.recordAuthentication:
                        self.enqueueEvent(clientId, None, {})
                    return api(*args)

            if secretKey in self.invalidCache:
                if self.invalidCache[secretKey] == clientId:
                    if self.debug:
                        print("Invalid Cache Hit")
                    return JsonResponse({"message": "Unauthorized"}, status=401)

            if self.debug:
                print("Authenticating at server")

            url = f"{self.baseUrl}/api/authenticate"
            headers = {
                KABLE_ENVIRONMENT_HEADER_KEY: self.environment,
                KABLE_CLIENT_ID_HEADER_KEY: self.kableClientId,
                X_CLIENT_ID_HEADER_KEY: clientId,
                X_API_KEY_HEADER_KEY: secretKey,
            }
            try:
                response = requests.post(url=url, headers=headers)
                status = response.status_code
                if (status == 200):
                    self.validCache.__setitem__(secretKey, clientId)
                    if self.recordAuthentication:
                        self.enqueueEvent(clientId, None, {})
                    return api(*args)
                else:
                    if status == 401:
                        self.invalidCache.__setitem__(secretKey, clientId)
                        return JsonResponse({"message": "Unauthorized"}, status=401)
                    else:
                        print("Unexpected " + status +
                              " response from Kable authenticate. Please update your SDK to the latest version immediately")
                        return JsonResponse({"message": "Something went wrong"}, status=500)

            except Exception as e:
                return JsonResponse({"message": "Something went wrong"}, status=500)

        return decoratedApi

    def enqueueEvent(self, clientId, customerId, data):
        event = {}
        event['environment'] = self.environment
        event['kableClientId'] = self.kableClientId
        event['clientId'] = clientId
        event['customerId'] = customerId
        event['timestamp'] = datetime.utcnow().isoformat()

        event['data'] = data

        library = {}
        library['name'] = 'kable-python-django'
        library['version'] = '2.3.6'
        event['library'] = library

        self.queue.append(event)
        if len(self.queue) >= self.maxQueueSize:
            self.flushQueue()

    def flushQueue(self):
        if self.debug:
            print("Flushing Kable event queue...")

        events = self.queue
        self.queue = []
        count = len(events)
        if (count > 0):
            if self.debug:
                print(f'Sending {count} batched events to server')

            url = f"{self.baseUrl}/api/events"
            headers = {
                KABLE_ENVIRONMENT_HEADER_KEY: self.environment,
                KABLE_CLIENT_ID_HEADER_KEY: self.kableClientId,
                X_CLIENT_ID_HEADER_KEY: self.kableClientId,
                X_API_KEY_HEADER_KEY: self.kableClientSecret
            }
            try:
                response = requests.post(
                    url=url, headers=headers, json=events)
                status = response.status_code
                if (status == 200):
                    print(
                        f'Successfully sent {count} events to Kable server')
                else:
                    print(f'Failed to send {count} events to Kable server')
                    for event in events:
                        print(f'Kable Event (Error): {event}')

            except Exception as e:
                print(f'Failed to send {count} events to Kable server')
                for event in events:
                    print(f'Kable Event (Error): {event}')

        else:
            if self.debug:
                print('...no Kable events to flush...')

    def startFlushQueueOnTimer(self):
        if self.debug:
            print('Starting time-based queue poller')

        self.killFlag = Event()
        self.queueFlushTimer = Kable.FlushTimer(self, self.killFlag)
        self.queueFlushTimer.start()

    def exitGracefully(self, *args):
        print(
            f'Kable will shut down gracefully within {self.queueFlushInterval} seconds')
        self.flushQueue()
        self.killFlag.set()
        sys.exit(0)

    class FlushTimer(Thread):
        def __init__(self, outer, killFlag):
            Thread.__init__(self)
            self.outer = outer
            self.stopped = killFlag

        def run(self):
            while not self.stopped.wait(10):
                self.outer.flushQueue()
