import os
import json
import requests
import warnings
import random
import time

from types import ModuleType

from .constants import HTTP_STATUS_CODE, ERROR_CODE, URL

from . import resources, utility

from .errors import (BadRequestError, GatewayError,
                     ServerError)


def capitalize_camel_case(string):
    return "".join(map(str.capitalize, string.split('_')))


# Create a dict of resource classes
RESOURCE_CLASSES = {}
for name, module in resources.__dict__.items():
    if isinstance(module, ModuleType) and \
            capitalize_camel_case(name) in module.__dict__:
        RESOURCE_CLASSES[name] = module.__dict__[capitalize_camel_case(name)]

UTILITY_CLASSES = {}
for name, module in utility.__dict__.items():
    if isinstance(module, ModuleType) and name.capitalize() in module.__dict__:
        UTILITY_CLASSES[name] = module.__dict__[name.capitalize()]


class Client:
    """Razorpay client class"""

    DEFAULTS = {
        'base_url': URL.BASE_URL,
        'max_retries': 5,
        'initial_delay': 1,
        'max_delay': 60,
        'jitter': 0.25
    }

    def __init__(self, session=None, auth=None, **options):
        """
        Initialize a Client object with session,
        optional auth handler, and options
        """
        self.session = session or requests.Session()
        self.auth = auth
        file_dir = os.path.dirname(__file__)
        self.cert_path = file_dir + '/ca-bundle.crt'

        self.base_url = self._set_base_url(**options)
        self.max_retries = options.get('max_retries', self.DEFAULTS['max_retries'])
        self.initial_delay = options.get('initial_delay', self.DEFAULTS['initial_delay'])
        self.max_delay = options.get('max_delay', self.DEFAULTS['max_delay'])
        self.jitter = options.get('jitter', self.DEFAULTS['jitter'])
        self.retry_enabled = False

        self.app_details = []

        # intializes each resource
        # injecting this client object into the constructor
        for name, Klass in RESOURCE_CLASSES.items():
            setattr(self, name, Klass(self))

        for name, Klass in UTILITY_CLASSES.items():
            setattr(self, name, Klass(self))

    def _set_base_url(self, **options):
        base_url = self.DEFAULTS['base_url']

        if 'base_url' in options:
            base_url = options['base_url']
            del(options['base_url'])

        # Remove retry options from options if they exist
        options.pop('max_retries', None)
        options.pop('initial_delay', None)
        options.pop('max_delay', None)
        options.pop('jitter', None)

        return base_url

    def _update_user_agent_header(self, options):
        user_agent = "{}{} {}".format('Razorpay-Python/', self._get_version(),
                                      self._get_app_details_ua())

        if 'headers' in options:
            options['headers']['User-Agent'] = user_agent
        else:
            options['headers'] = {'User-Agent': user_agent}

        return options

    def _get_version(self):
        version = ""
        try: # nosemgrep : gitlab.bandit.B110
            # Try importlib.metadata first (modern approach)
            try:
                import importlib.metadata
                from importlib.metadata import PackageNotFoundError
                version = importlib.metadata.version("razorpay")
            except ImportError:
                # Fall back to pkg_resources
                import pkg_resources
                from pkg_resources import DistributionNotFound
                version = pkg_resources.require("razorpay")[0].version
        except (PackageNotFoundError, DistributionNotFound, NameError):  # pragma: no cover
            # PackageNotFoundError: importlib.metadata couldn't find the package
            # DistributionNotFound: pkg_resources couldn't find the package  
            # NameError: in case the exception classes aren't defined due to import issues
            
            # If all else fails, use the hardcoded version from the package
            version = "1.4.3"

            warnings.warn(
              "Could not detect razorpay package version. Using fallback version."
              "This may indicate an installation issue.",
              UserWarning,
              stacklevel=4
            )
        return version

    def _get_app_details_ua(self):
        app_details_ua = ""

        app_details = self.get_app_details()

        for app in app_details:
            if 'title' in app:
                app_ua = app['title']
                if 'version' in app:
                    app_ua += "/{}".format(app['version'])
                app_details_ua += "{} ".format(app_ua)

        return app_details_ua

    def set_app_details(self, app_details):
        self.app_details.append(app_details)

    def get_app_details(self):
        return self.app_details

    def enable_retry(self, retry_enabled=False):
        self.retry_enabled = retry_enabled
        
    def request(self, method, path, **options):
        """
        Dispatches a request to the Razorpay HTTP API with retry mechanism
        """
        options = self._update_user_agent_header(options)

        # Determine authentication type
        use_public_auth = options.pop('use_public_auth', False)
        auth_to_use = self.auth

        if use_public_auth:
            # For public auth, use key_id only
            if self.auth and isinstance(self.auth, tuple) and len(self.auth) >= 1:
                auth_to_use = (self.auth[0], '')  # Use key_id only, empty key_secret

        # Inject device mode header if provided
        device_mode = options.pop('device_mode', None)
        if device_mode is not None:
            if 'headers' not in options:
                options['headers'] = {}
            options['headers']['X-Razorpay-Device-Mode'] = device_mode

        url = "{}{}".format(self.base_url, path)
        
        delay_seconds = self.initial_delay
        
        # If retry is not enabled, set max attempts to 1
        max_attempts = self.max_retries if self.retry_enabled else 1
        
        for attempt in range(max_attempts):
            try:
                response = getattr(self.session, method)(url, auth=auth_to_use,
                                                         verify=self.cert_path,
                                                         **options)
                
                if ((response.status_code >= HTTP_STATUS_CODE.OK) and
                        (response.status_code < HTTP_STATUS_CODE.REDIRECT)):
                    return json.dumps({}) if(response.status_code==204) else response.json()
                else:
                    msg = ""
                    code = ""
                    json_response = response.json()
                    if 'error' in json_response:
                        if 'description' in json_response['error']:
                            msg = json_response['error']['description']
                        if 'code' in json_response['error']:
                            code = str(json_response['error']['code'])

                    if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR:
                        raise BadRequestError(msg)
                    elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR:
                        raise GatewayError(msg)
                    elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
                        raise ServerError(msg)
                    else:
                        raise ServerError(msg)
                        
            except requests.exceptions.ConnectionError as e:
                if self.retry_enabled and attempt < max_attempts - 1:  # Don't sleep on the last attempt
                    # Apply exponential backoff with jitter
                    jitter_value = random.uniform(-self.jitter, self.jitter)
                    jittered_delay = delay_seconds * (1 + jitter_value)
                    # Cap the delay at max_delay
                    actual_delay = min(jittered_delay, self.max_delay)
                    
                    print(f"ConnectionError: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
                    time.sleep(actual_delay)
                    delay_seconds *= 2  # Exponential backoff for next attempt
                else:
                    print(f"Connection failed." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
                    raise
            except requests.exceptions.Timeout as e:
                if self.retry_enabled and attempt < max_attempts - 1:  # Don't sleep on the last attempt
                    # Apply exponential backoff with jitter
                    jitter_value = random.uniform(-self.jitter, self.jitter)
                    jittered_delay = delay_seconds * (1 + jitter_value)
                    # Cap the delay at max_delay
                    actual_delay = min(jittered_delay, self.max_delay)
                    
                    print(f"Timeout: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
                    time.sleep(actual_delay)
                    delay_seconds *= 2  # Exponential backoff for next attempt
                else:
                    print(f"Request timed out." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
                    raise
            except requests.exceptions.RequestException as e:
                # For other request exceptions, don't retry
                print(f"Request error occurred: {e}")
                raise

    def get(self, path, params, **options):
        """
        Parses GET request options and dispatches a request
        """
        return self.request('get', path, params=params, **options)

    def post(self, path, data, **options):
        """
        Parses POST request options and dispatches a request
        """
        data, options = self._update_request(data, options)
        return self.request('post', path, data=data, **options)

    def patch(self, path, data, **options):
        """
        Parses PATCH request options and dispatches a request
        """
        data, options = self._update_request(data, options)
        return self.request('patch', path, data=data, **options)

    def delete(self, path, data, **options):
        """
        Parses DELETE request options and dispatches a request
        """
        data, options = self._update_request(data, options)
        return self.request('delete', path, data=data, **options)

    def put(self, path, data, **options):
        """
        Parses PUT request options and dispatches a request
        """
        data, options = self._update_request(data, options)
        return self.request('put', path, data=data, **options)

    def file(self, path, data, **options):     
        fileDict = {}
        fieldDict = {}
        
        if('file' not in data):
            # if file is not exists in the dictionary
            data['file'] = ""

        fileDict['file'] = data['file'] 
        
        # Create a dict of form fields 
        for fields in data:
          if(fields != 'file'):
            fieldDict[str(fields)] = data[fields] 

        return self.request('post', path, files=fileDict, data=fieldDict, **options)

    def _update_request(self, data, options):
        """
        Updates The resource data and header options
        """
        data = json.dumps(data)

        if 'headers' not in options:
            options['headers'] = {}

        options['headers'].update({'Content-type': 'application/json'})

        return data, options
