import base64
import json
from typing import Any, Type, Optional

import requests
from pydantic import BaseModel, Field

from codemie_tools.base.codemie_tool import CodeMieTool
from codemie_tools.base.utils import parse_to_dict
from codemie_tools.open_api.models import OpenApiConfig
from codemie_tools.open_api.tools_vars import OPEN_API_TOOL, OPEN_API_SPEC_TOOL


def _get_auth_header_value(config) -> str:
    if config.is_basic_auth and config.username:
        credentials = f"{config.username}:{config.api_key}"
        encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
        return f"Basic {encoded_credentials}"
    return config.api_key


class OpenApiInput(BaseModel):
    method: str = Field(
        description="""
            String text. It's the verb of a HTTP request generated by LLM which 
            will be used to invoke appropriate REST API to fulfill user request. 
            Important: string formatting and escaping SHOULD NOT be used when passing query to the tool.
            """.strip(),
    )
    url: str = Field(
        description="""
            String text. It's the URL of a HTTP request generated by LLM which 
            will be used to invoke appropriate REST API to fulfill user request. 
            Important: string formatting and escaping SHOULD NOT be used when passing query to the tool.
            """.strip(),
    )
    headers: Optional[str] = Field(
        default="",
        description="""
            MUST be String text. It's the headers of a HTTP request generated by LLM which
            will be used to invoke appropriate REST API to fulfill user request.
            Important: string formatting and escaping SHOULD NOT be used when passing query to the tool.
            """.strip(),
    )
    fields: Optional[str] = Field(
        default="",
        description="""
            MUST be String text. It's the query parameters of a HTTP request generated by LLM which 
            will be used to invoke appropriate REST API to fulfill user request.
            Important: string formatting and escaping SHOULD NOT be used when passing query to the tool.
             """.strip(),
    )
    body: Optional[str] = Field(
        default="",
        description="""
            JSON that should be passed as a string. It's the body of a HTTP request generated by LLM which 
            will be used to invoke appropriate REST API to fulfill user request. 
            Should be passes as an empty string if there is no body. 
            Important: string formatting and escaping SHOULD NOT be used when passing query to the tool.
            """.strip(),
    )
    filter_fields: Optional[str] = Field(
        default="",
        description="""
            Optional. Dot notation paths to fields that should be extracted from the response JSON.
            Multiple fields can be specified as a comma-separated list, e.g., "transcription,author,data.people".
            If specified, only these fields will be returned from the response.
            Nested fields can be accessed using dot notation, e.g., "data.people".
            """.strip(),
    )

class GetOpenApiSpecToolInput(BaseModel):
    query: Optional[str] = Field(
        default="",
        description="User initial request should be passed as a string.",
    )


class InvokeRestApiBySpec(CodeMieTool):
    """Tool for invoking REST APIs based on OpenAPI specifications."""
    config: OpenApiConfig = Field(exclude=True, default=None)
    name: str = OPEN_API_TOOL.name
    description: str = OPEN_API_TOOL.description
    args_schema: Type[BaseModel] = OpenApiInput
    tokens_size_limit: int = 20000  # Increased token limit to 20000

    def __init__(self, config: OpenApiConfig = None, **kwargs):
        super().__init__(**kwargs)
        self.config = config
        self.metadata = {}

    def _format_error_response(self, error_type: str, error_details: str) -> str:
        """Format error responses consistently"""
        return f"Error ({error_type}): {error_details}"

    def _navigate_to_field(self, data, parts, current_index=0):
        """Navigate to a specific nested field in the data structure."""
        if current_index >= len(parts) or parts[current_index] not in data:
            return None
        
        if current_index == len(parts) - 1:
            return data[parts[current_index]]
            
        return self._navigate_to_field(data[parts[current_index]], parts, current_index + 1)
    
    def _extract_single_field(self, json_data, field_path, result):
        """Extract a single field from JSON data based on dot notation path."""
        parts = field_path.split('.')
        
        try:
            # Navigate through the path to find the value
            value = self._navigate_to_field(json_data, parts)
            if value is None:
                return
                
            # Build the result structure
            current_dict = result
            for part in parts[:-1]:
                current_dict = current_dict.setdefault(part, {})
            
            current_dict[parts[-1]] = value
        except (KeyError, TypeError):
            # Skip fields that don't exist or can't be accessed
            pass
            
    def _extract_filtered_fields(self, json_data, filter_fields):
        """Extract specific fields from a JSON response using dot notation."""
        if not filter_fields:
            return json_data
            
        # Parse the filter fields string into a list
        fields = [field.strip() for field in filter_fields.split(',')]
        result = {}
        
        for field_path in fields:
            self._extract_single_field(json_data, field_path, result)
                
        return result if result else json_data  # Return original if no fields were found

    def _prepare_headers(self, headers, fields):
        """Prepare request headers and parameters."""
        headers_param = parse_to_dict(headers) if headers else {}
        params = parse_to_dict(fields) if fields else None
    
        # Add callback_id to headers if available
        callback_id = self.metadata.get('callback_id') if hasattr(self, 'metadata') else None
        if callback_id:
            headers_param['callback_id'] = callback_id
    
        # Add authentication if API key exists and is not empty
        if self.config.api_key and self.config.api_key.strip():
            # Use auth_header_name but default to 'Authorization' if it's None or empty
            auth_header = self.config.auth_header_name or 'Authorization'
            headers_param[auth_header] = _get_auth_header_value(config=self.config)
    
        return headers_param, params

    def _handle_response(self, response, filter_fields):
        """Process the API response."""
        # If filter_fields is provided and response is JSON, filter the response
        is_json = response.headers.get('Content-Type', '').startswith('application/json')
        
        if filter_fields and is_json:
            try:
                json_data = response.json()
                filtered_data = self._extract_filtered_fields(json_data, filter_fields)
                return json.dumps(filtered_data)
            except json.JSONDecodeError:
                return response.text
        
        # Return the raw text response if no filtering needed
        return response.text

    def _make_request(self, method, url, params, headers_param, body):
        """Make the HTTP request and return the response."""
        return requests.request(
            method=method,
            url=url,
            params=params,
            headers=headers_param,
            data=body if body else None,
            timeout=self.config.timeout,
            verify=False  # For internal tools and usage
        )

    def _handle_exception(self, exception, url):
        """Handle different types of request exceptions."""
        if isinstance(exception, requests.exceptions.HTTPError):
            status_code = exception.response.status_code if exception.response else "Unknown"
            error_content = exception.response.text if exception.response else str(exception)
            return self._format_error_response(
                f"HTTP Error {status_code}",
                f"The server returned an error: {error_content}"
            )
        elif isinstance(exception, requests.exceptions.ConnectionError):
            return self._format_error_response(
                "Connection Error",
                f"Failed to establish a connection to {url}"
            )
        elif isinstance(exception, requests.exceptions.Timeout):
            return self._format_error_response(
                "Timeout Error",
                f"The request to {url} timed out"
            )
        elif isinstance(exception, requests.exceptions.RequestException):
            return self._format_error_response(
                "Request Error",
                f"An error occurred during the request: {str(exception)}"
            )
        else:
            return self._format_error_response(
                "Unexpected Error",
                f"An unexpected error occurred: {str(exception)}"
            )

    def execute(self, method: str, url: str,
                headers: Any = "", fields: Any = "", body: str = "",
                filter_fields: str = "", **kwargs: Any) -> Any:
        # Prepare headers and parameters
        headers_param, params = self._prepare_headers(headers, fields)

        try:
            # Make the request
            response = self._make_request(method, url, params, headers_param, body)
            
            # Check if the request was successful
            response.raise_for_status()
            
            # Process and return the response
            return self._handle_response(response, filter_fields)

        except Exception as e:
            return self._handle_exception(e, url)


class GetOpenApiSpec(CodeMieTool):
    openapi_spec: str = Field(exclude=True, default=None)
    name: str = OPEN_API_SPEC_TOOL.name
    description: str = OPEN_API_SPEC_TOOL.description
    args_schema: Type[BaseModel] = GetOpenApiSpecToolInput

    def __init__(self, config: OpenApiConfig):
        super().__init__()
        self.openapi_spec = config.spec

    def execute(self, *args, **kwargs: Any) -> Any:
        if not self.openapi_spec:
            return "No OpenAPI specification provided. Please configure the OpenAPI specification in the tool settings."
        return self.openapi_spec
