from dotenv import dotenv_values
import requests
from enum import Enum
import json
import base64
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from concurrent.futures import ThreadPoolExecutor, as_completed

class AuthenticationErrorCode(Enum):
    Unknown = 0
    BadAPIKey = 1
    MissingAuthenticationHeaders = 2
    RefreshTokenNeedsRenewal = 3
    AccessTokenNeedsRenewal = 4
    RenewalNonceExpired = 5
    TokenParserError = 6
    InvalidLoginCredentials = 7
    BannedAccount = 8
    BannedIP = 9
    BannedDevice = 10
    RequiresVerifiedEmailAddress = 11

class BigApiError(Exception):
    pass

def requestsRetrySession(retries=10, backoff_factor=0.3, status_forcelist=(500, 502, 504), session=None):
    session = session or requests.Session()
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    return session

class BigApi:
    refreshToken = ""
    accessToken = ""
    authApiUrl = ""
    adminApiUrl = ""
    cloudApiUrl = ""
    cloudWssUrl = ""
    apiBearerToken = ""
    adminEmail = ""
    adminPassword = ""
    accessToken = ""
    refreshToken = ""

    @staticmethod
    def init(pathToEnvironmentFile):
        config = dotenv_values(pathToEnvironmentFile)

        # The auth server is used for authentication and account management
        BigApi.authApiUrl = config['BIGSCREEN_API_URL']

        # The admin server has the fabricator endpoints
        BigApi.adminApiUrl = config['BIGSCREEN_ADMIN_API_URL']

        BigApi.cloudApiUrl = config['BIGSCREEN_CLOUD_API_URL']

        BigApi.cloudWssUrl = config['BIGSCREEN_WSS_URL']

        # Each http request needs a bearer token. This is a revokable token which is assigned to each external application that
        # uses the Bigscreen APIs (e.g. this script is an external application).
        BigApi.apiBearerToken = config['BIGSCREEN_API_KEY']

        # Finally, we need an account.  The account is necessary for us to identify who is using the various APIs. The account 
        # must have access to the "Fabricator" endpoints to work.
        BigApi.adminEmail = config['ADMIN_ACCOUNT_EMAIL']
        BigApi.adminPassword = config['ADMIN_ACCOUNT_PASSWORD']

    @staticmethod
    def getAuthHeaders():
        return {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {BigApi.apiBearerToken}"
        }

    @staticmethod
    def getAdminHeaders():
        return {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {BigApi.apiBearerToken}",
            "x-access-token": BigApi.accessToken
        }

    @staticmethod
    def login(email, password):
        payload = {
            "email": email,
            "password": password
        }
        print(BigApi.authApiUrl)
        r = requestsRetrySession().post(f"{BigApi.authApiUrl}/login", headers=BigApi.getAuthHeaders(), json=payload)
        print(r.status_code)
        BigApi.refreshToken = r.headers["x-refresh-token"]
        BigApi.accessToken = r.headers["x-access-token"]
        assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code} {r.text})"
        r = requestsRetrySession().get(f"{BigApi.authApiUrl}/verify", headers=BigApi.getAdminHeaders())
        assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code} {r.text})"

    @staticmethod
    def adminLogin():
        payload = {
            "email": BigApi.adminEmail,
            "password": BigApi.adminPassword
        }

        r = requestsRetrySession().post(f"{BigApi.authApiUrl}/login", headers=BigApi.getAuthHeaders(), json=payload)
        print(r.status_code)
        BigApi.refreshToken = r.headers["x-refresh-token"]
        BigApi.accessToken = r.headers["x-access-token"]
        r = requestsRetrySession().get(f"{BigApi.authApiUrl}/verify", headers=BigApi.getAdminHeaders())

    @staticmethod
    def getSystemInfo():
        ip = "unknown"
        try:
            res = requestsRetrySession().get(f"{BigApi.authApiUrl}/auth/whoami", headers=BigApi.getAuthHeaders())
            ip = res.json()["ip"]
        except:
            pass
        systemInfo = {
            "ip": ip,
            "deviceUniqueIdentifier": f"python_notebook_{ip}",
            "deviceName": "python_notebook"
        }
        return base64.b64encode(json.dumps(systemInfo).encode("utf-8")).decode("utf-8")
    
    @staticmethod
    def renewAccessToken(nonce):
        if (BigApi.refreshToken == ""):
            raise BigApiError("Refresh token was not set. Login required.")
    
        headers = BigApi.getAuthHeaders()
        headers["x-bigscreen-nonce"] = nonce
        headers["x-refresh-token"] = BigApi.refreshToken
        headers["x-bigscreen-system-info"] = BigApi.getSystemInfo()
        res = requestsRetrySession().get(f"{BigApi.authApiUrl}/auth/renew", headers=headers)

        if (res.status_code == 200):
            BigApi.refreshToken = res.headers["x-refresh-token"]
            BigApi.accessToken = res.headers["x-access-token"]
            print("SUCCESS THE TOKEN WAS RENEWED AND THE NEXT REQUEST SHOULD FUCKING WORK.")
            return True
        else:
            print(res.json())
            print("Error renewing the token. Weird.")
            return False

    @staticmethod
    def handleExpiredToken(res):
        if (res.status_code == 401):
            try:
                errorResponse = res.json()
                if (errorResponse["code"]):
                    authenticationErrorCode = errorResponse["code"]
                    if (authenticationErrorCode == AuthenticationErrorCode.AccessTokenNeedsRenewal):
                        nonce = res.headers["x-bigscreen-nonce"]
                        if (nonce):
                            renewalResult = BigApi.renewAccessToken(nonce)
                            return renewalResult
                        else:
                            raise BigApiError("Nonce not found in response")
            except:
                print(res.text)
                return False
        else:
            print(res.json())
        return False
    
    @staticmethod
    def checkAccessTokenStatus():
        if BigApi.accessToken == "":
            return False

        res = requestsRetrySession().get(f"{BigApi.authApiUrl}/auth/verify", headers=BigApi.getAdminHeaders())
        if (res.status_code == 200):
            return True
        
        return BigApi.handleExpiredToken(res)


    @staticmethod
    def apiGet(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().get(f"{BigApi.authApiUrl}{url}", headers=BigApi.getAdminHeaders(), timeout=300)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.apiGet(url)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def apiPost(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().post(f"{BigApi.authApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.apiPost(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def apiPut(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().put(f"{BigApi.authApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.apiPut(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def adminDel(url):
        BigApi.checkAccessTokenStatus()
        r = requests.delete(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders())
        if r.status_code == 200:
            return r.json()
        else:
            if (r.status_code == 422):
                return r.json()
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def adminGet(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().get(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders(), timeout=300)
        print(r.status_code)
        if r.status_code == 200:
            try:
                return r.json()
            except:
                return r.text
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminGet(url)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            if (r.status_code == 422):
                try:
                    return r.json()
                except:
                    return r.text
            if (r.status_code == 500):
                try:
                    return r.json()
                except:
                    return r.text
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def adminGetRaw(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().get(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders(), timeout=300)
        if r.status_code == 200:
            return r
        
    @staticmethod
    def adminGetWithoutRetry(url):
        BigApi.checkAccessTokenStatus()
        r = requests.get(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders(), timeout=600)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminGet(url)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"
        
    @staticmethod
    def adminGetAll(url_dict):
        BigApi.checkAccessTokenStatus()
        results = {}
        with ThreadPoolExecutor(max_workers=len(url_dict)) as executor:
            future_to_key = {executor.submit(BigApi.adminGet, url): key for key, url in url_dict.items()}
            for future in as_completed(future_to_key):
                key = future_to_key[future]
                try:
                    result = future.result()
                    results[key] = result
                except Exception as e:
                    print(f"Exception: {e}")
        return results

    @staticmethod
    def adminPost(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().post(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminPost(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        elif r.status_code == 422:
            print (r.text)
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def adminPut(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().put(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            try:
                return r.json()
            except:
                return r.text
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminPut(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(r.text)
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"
            
    @staticmethod
    def adminDelete(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().delete(f"{BigApi.adminApiUrl}{url}", headers=BigApi.getAdminHeaders())
        if r.status_code == 200:
            try:
                return r.json()
            except:
                return r.text
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminDelete(url)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            if (r.status_code == 422):
                return r.json()
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def cloudGet(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().get(f"{BigApi.cloudApiUrl}{url}", headers=BigApi.getAdminHeaders())
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.cloudGet(url)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def cloudGetRaw(url):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().get(f"{BigApi.cloudApiUrl}{url}", headers=BigApi.getAdminHeaders())
        if r.status_code == 200:
            return r
        try:
            return r.json()
        except:
            return r.text
        
    @staticmethod
    def cloudPost(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().post(f"{BigApi.cloudApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            return r.json()
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.cloudPost(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        elif r.status_code == 422:
            print (r.text)
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"
        else:
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"
    
    @staticmethod
    def cloudPut(url, payload):
        BigApi.checkAccessTokenStatus()
        r = requestsRetrySession().put(f"{BigApi.cloudApiUrl}{url}", headers=BigApi.getAdminHeaders(), json=payload)
        if r.status_code == 200:
            try:
                return r.json()
            except:
                return r.text
        elif r.status_code == 401:
            if (BigApi.handleExpiredToken(r)):
                return BigApi.adminPut(url, payload)
            else:
                raise BigApiError("Access token expired and could not be renewed")
        else:
            print(r.text)
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"

    @staticmethod
    def cloudDel(url):
        BigApi.checkAccessTokenStatus()
        r = requests.delete(f"{BigApi.cloudApiUrl}{url}", headers=BigApi.getAdminHeaders())
        if r.status_code == 200:
            return r.json()
        else:
            if (r.status_code == 422):
                return r.json()
            print(f"status code should be 200 (actual: {r.status_code})")
            assert r.status_code == 200, f"status code should be 200 (actual: {r.status_code})"