import requests
import time
from datetime import datetime, timedelta

# Constants
AUTH_URL = 'https://idp.test.bluestonepim.com/op/token'
CLIENT_ID = 'YOUR_ID'
CLIENT_SECRET = 'YOUR_SECRET'
BASE_URL = "https://api.test.bluestonepim.com"
TIMEOUT = 10  # Request timeout in seconds
RETRY_STATUSES = {429, 500, 502, 503, 504} # HTTP response statuses to retry
TOKEN_RETRIES = 3  # Number of retries for fetching the token

# Global variable to store token information
token_info = None

# This function is responsible for obtaining an authentication token from the authentication server.
# It first checks if there's an existing token that hasn't expired yet, reusing it if possible to avoid unnecessary requests.
# If no valid token is available, it makes a POST request to the authentication server, retrying up to TOKEN_RETRIES times if certain error conditions are encountered (like a timeout or one of the specified retryable HTTP status codes).
# A simple 3-second wait is introduced before retrying to fetch the token if a retryable error occurs.
# If a token is successfully obtained, the function updates the global token_info variable with the new token and its expiry time, then returns the token.
# If the maximum number of retries is exceeded or other request errors occur, it prints an error message to the console and raises the exception to halt execution.
def get_token():
    global token_info
    # If we have a token and it's not expired, reuse it
    if token_info and token_info['expiry_time'] > datetime.now():
        return token_info['access_token']

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
    }
    data = {
        'grant_type': 'client_credentials',
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
    }
    for retry in range(TOKEN_RETRIES):
        try:
            response = requests.post(AUTH_URL, headers=headers, data=data, timeout=TIMEOUT)
            if response.status_code in RETRY_STATUSES and retry < TOKEN_RETRIES - 1:
                print(f"Received {response.status_code} while fetching token, waiting 3 seconds before retrying...")
                time.sleep(3) 
                continue
            response.raise_for_status()
            auth_json = response.json()
            expiry_time = datetime.now() + timedelta(seconds=auth_json['expires_in'])
            token_info = {'access_token': auth_json['access_token'], 'expiry_time': expiry_time}
            return token_info['access_token']
        except requests.exceptions.RequestException as e:
            print(f"An error occurred while fetching token: {e}")
            if retry >= TOKEN_RETRIES - 1:
                print("Max retries to get token exceeded.")
                raise e

# This function is designed to make a request to a specified endpoint of the API.
# It first calls get_token to ensure it has a valid authentication token.
# It then attempts to make the API request, retrying up to a specified number of times (retries) if a retryable error condition occurs (like a 429 Too Many Requests or certain 500-level server errors).
# An exponential backoff strategy is employed to wait increasingly longer amounts of time before each retry, helping to alleviate load on the server and increase the likelihood of a successful request on subsequent retries.
# If a request error occurs, it prints an error message along with the method parameters to the console for debugging purposes, and then raises the exception to halt execution.
def request_api(endpoint, method, payload=None, retries=5, backoff_factor=2):
    url = f"{BASE_URL}/{endpoint}"
    
    access_token = get_token()   
    
    for retry in range(retries):
        
        headers = {
            'context-fallback': 'true',
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {access_token}'
        }
        try:
            response = requests.request(method, url, headers=headers, json=payload, timeout=TIMEOUT)
            # Only retry for 429 Too Many Requests or certain 5xx server errors
            if response.status_code in RETRY_STATUSES and retry < retries - 1:
                print(f"Received {response.status_code}, retrying...")
                
                # Apply exponential backoff 
                time.sleep(backoff_factor ** retry) 
                continue
            response.raise_for_status()  # Raise HTTPError for bad responses (4xx and 5xx)
            return response.json()
        except requests.exceptions.RequestException as e:            
            print(f"An error occurred: {e}")
            print("Method parameters", endpoint, method, payload)
            raise e

# Usage
endpoint = "pim/catalogs"
method = "GET"
response_json = request_api(endpoint, method)
print(response_json)