import httpx
import base64
import json
from typing import Dict, Any
import re

class APIRequestBuilder:
    async def build_request(self, app_details: Dict[str, Any], request_data: Dict[str, Any], 
                            app_integration: Dict[str, str], openapi_spec: Dict[str, Any], flow: Dict[str, str]) -> httpx.Request:

        auth_config = app_integration['auth_config'] # Load Authentication details 
        configurations = app_integration['configurations'] # Load params details

        base_url = openapi_spec.get("servers", [{}])[0].get("url", "").rstrip("/")
        auth_config['token_url'] = openapi_spec.get("servers", [{}])[0].get("token_url", "").rstrip("/")
        function_name = flow['endpoint'] #function_name = request_data.get("f", "")
        normalized_function_name = function_name if function_name.startswith("/") else f"/{function_name}"
        full_url = f"{base_url}{normalized_function_name}"

        #Making dynamic urls ex, passing twitter ID in the URL or version in Salesforce. 
        for key in list(configurations):  # Create a list of keys to safely modify the dictionary during iteration
            if f"{{{key}}}" in full_url:
                full_url = full_url.replace(f"{{{key}}}", configurations[key])
                del configurations[key]  # Remove the key from the dictionary after use
        full_url = re.sub(r'(?<!http:)(?<!https:)//', '/', full_url)

        #path_config = openapi_spec['paths'].get(normalized_function_name, {})
        path_config = openapi_spec['paths'].get(function_name, {}) or openapi_spec['paths'].get(normalized_function_name)

        http_method = 'POST' if 'post' in path_config else 'GET'
        content_type = "application/json" if http_method == "POST" else "application/x-www-form-urlencoded"
        response_schema = path_config.get(http_method.lower(), {}).get("responses", {})

        #API header value
        headers_schema = openapi_spec.get("components", {}).get("schemas", {}).get("Headers", {}).get("properties", {})
        if headers_schema:
            auth_header_value = headers_schema.get("Authorization", {}).get("value", "")
            if headers_schema.get("ContentType"):
                content_type = headers_schema.get("ContentType").get("value")
        else:
            auth_header_value = 'Bearer'

        auth_config['auth_header_value'] = auth_header_value

        # Determine HTTP Method

        # Parameters and Body
        default_params = configurations
        user_params = request_data

        # Merge parameters for GET requests or set JSON body for POST requests
        if http_method == "GET":
            params = {**default_params, **user_params}
            json_body = None
        else:
            params = None
            json_body = {**default_params, **user_params}

        headers = await self.construct_auth_headers(auth_config)
        headers['Content-Type'] = content_type

        #print( " Request :: ", full_url, " :: , " , headers, " : ", params, " : ", json_body )
        return httpx.Request(http_method, full_url, headers=headers, params=params, json=json_body), response_schema

    async def construct_auth_headers(self, auth_config: Dict[str, str]) -> Dict[str, str]:
        headers = {}
        credentials = auth_config.get('credentials', {})
        auth_header_value = auth_config.get('auth_header_value')

        # OAuth handling
        if 'client_id' in auth_config and 'client_secret' in credentials:
            access_token = await self.handle_oauth(auth_config)
            headers['Authorization'] = f'{auth_header_value} {access_token}'

        # Handle other authentication methods
        elif 'auth_token' in auth_config:
            headers['Authorization'] = f"{auth_header_value}  {credentials['auth_token']}"

        elif 'username' in auth_config and 'password' in auth_config:
            basic_auth = base64.b64encode(f"{auth_config['username']}:{auth_config['password']}".encode()).decode()
            headers['Authorization'] = f"{auth_header_value} {basic_auth}"

        elif 'api_key' or 'apikey' or 'api-key' in auth_config:
            key_value = ''
            for key, value in credentials.items():
                key_value = value
            #headers['X-API-KEY'] = auth_config['api_key']
            headers['Authorization'] = f"{auth_header_value} {key_value}"

        elif 'email' in auth_config and 'api_token' in credentials: #Zendesk
            basic_auth_credentials = f"{auth_config['email']}:{credentials['api_token']}"
            basic_auth_encoded = base64.b64encode(basic_auth_credentials.encode()).decode()
            headers['Authorization'] = f"{auth_header_value} {basic_auth_encoded}"

        return headers

    async def handle_oauth(self, auth_config: Dict[str, str]) -> str:

        credentials = auth_config.get('credentials', {})

        # Refresh token if available
        refresh_token = credentials.get('refresh_token')
        client_id = credentials['client_id']
        client_secret = credentials['client_secret']

        token_url = auth_config.get('token_url')

        if refresh_token:
            token_response = await self.refresh_oauth_token(client_id, client_secret, refresh_token, token_url)
            new_access_token = token_response.get('access_token') or token_response.get('accesstoken') or token_response
            return new_access_token

        raise Exception("OAuth token could not be obtained.")

    async def refresh_oauth_token(self, client_id: str, client_secret: str, 
                                  refresh_token: str, token_url: str) -> Dict[str, Any]:
        payload = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': client_id,
            'client_secret': client_secret
        }
        async with httpx.AsyncClient() as client:
            response = await client.post(token_url, data=payload)
            if response.status_code == 200:
                return response.json()
            else:
                raise Exception(f"Failed to refresh token: {response.text}")

    async def test_request(self, request_data: Dict[str, Any], 
                           openapi_spec: Dict[str, Any], function_name: str, 
                           configurations: Dict[str, str], auth_config: Dict[str, Any]) -> (httpx.Request, Dict[str, Any]):

        # Extract the base URL from 'app_details' or 'openapi_spec'
        base_url = openapi_spec.get("servers", [{}])[0].get("url", "").rstrip("/")
        auth_config['token_url'] = openapi_spec.get("servers", [{}])[0].get("token_url", "").rstrip("/")
        normalized_function_name = function_name if function_name.startswith("/") else f"/{function_name}"
        full_url = f"{base_url}{normalized_function_name}"

        # Construct the full URL, replacing placeholders with actual values from configurations
        full_url = f"{base_url}/{function_name.lstrip('/')}"
        for key in list(configurations):  # Create a list of keys to safely modify the dictionary during iteration
            if f"{{{key}}}" in full_url:
                full_url = full_url.replace(f"{{{key}}}", configurations[key])
                del configurations[key]  # Remove the key from the dictionary after use
        full_url = re.sub(r'(?<!http:)(?<!https:)//', '/', full_url)
        full_url = re.sub(r'(?<!:)//', '/', full_url)

        #path_config = openapi_spec['paths'].get(normalized_function_name, {})
        path_config = openapi_spec['paths'].get(function_name, {}) or openapi_spec['paths'].get(normalized_function_name)

        http_method = 'POST' if 'post' in path_config else 'GET'
        content_type = "application/json" if http_method == "POST" else "application/x-www-form-urlencoded"
        headers_schema = openapi_spec.get("components", {}).get("schemas", {}).get("Headers", {}).get("properties", {})
        if headers_schema:
            auth_header_value = headers_schema.get("Authorization", {}).get("value", "")
            if headers_schema.get("ContentType"):
                content_type = headers_schema.get("ContentType").get("value")
        else:
            auth_header_value = 'Bearer'

        auth_config['auth_header_value'] = auth_header_value


        # Prepare headers and authenticate
        headers = await self.construct_auth_headers(auth_config)
        headers['Content-Type'] = content_type

        #API header value
        # Setup request parameters or JSON body based on the HTTP method
        if http_method == "GET":
            params = {**configurations, **request_data}
            json_body = None
        else:
            params = None
            json_body = {**configurations, **request_data}

        print("Request :: ", full_url, " :: , ", headers, " : ", params, " : ", json_body)
        # Construct the httpx.Request object
        request = httpx.Request(method=http_method, url=full_url, headers=headers, params=params, json=json_body)

        # Extract the expected response schema from the OpenAPI spec for validation
        response_schema = path_config.get(http_method.lower(), {}).get('responses', {})

        return request, response_schema

