from typing import Any, Dict, List, Optional, Type

from litellm._logging import verbose_proxy_logger


class CustomOpenAPISpec:
    """
    Handler for customizing OpenAPI specifications with Pydantic models
    for documentation purposes without runtime validation.
    """
    
    CHAT_COMPLETION_PATHS = [
        "/v1/chat/completions",
        "/chat/completions", 
        "/engines/{model}/chat/completions",
        "/openai/deployments/{model}/chat/completions"
    ]
    
    EMBEDDING_PATHS = [
        "/v1/embeddings",
        "/embeddings",
        "/engines/{model}/embeddings", 
        "/openai/deployments/{model}/embeddings"
    ]
    
    RESPONSES_API_PATHS = [
        "/v1/responses",
        "/responses"
    ]
    
    @staticmethod
    def get_pydantic_schema(model_class) -> Optional[Dict[str, Any]]:
        """
        Get JSON schema from a Pydantic model, handling both v1 and v2 APIs.
        
        Args:
            model_class: Pydantic model class
            
        Returns:
            JSON schema dict or None if failed
        """
        try:
            # Try Pydantic v2 method first
            return model_class.model_json_schema()  # type: ignore
        except AttributeError:
            try:
                # Fallback to Pydantic v1 method
                return model_class.schema()  # type: ignore
            except AttributeError:
                # If both methods fail, return None
                return None
        except Exception as e:
            # FastAPI 0.120+ may fail schema generation for certain types (e.g., openai.Timeout)
            # Log the error and return None to skip schema generation for this model
            verbose_proxy_logger.debug(f"Failed to generate schema for {model_class}: {e}")
            return None
    
    @staticmethod
    def add_schema_to_components(openapi_schema: Dict[str, Any], schema_name: str, schema_def: Dict[str, Any]) -> None:
        """
        Add a schema definition to the OpenAPI components/schemas section.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            schema_name: Name for the schema component
            schema_def: The schema definition
        """
        # Ensure components/schemas structure exists
        if "components" not in openapi_schema:
            openapi_schema["components"] = {}
        if "schemas" not in openapi_schema["components"]:
            openapi_schema["components"]["schemas"] = {}
        
        # Add the schema
        CustomOpenAPISpec._move_defs_to_components(openapi_schema, {schema_name: schema_def})
    
    @staticmethod
    def add_request_body_to_paths(openapi_schema: Dict[str, Any], paths: List[str], schema_ref: str) -> None:
        """
        Add request body with expanded form fields for better Swagger UI display.
        This keeps the request body but expands it to show individual fields in the UI.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            paths: List of paths to update
            schema_ref: Reference to the schema component (e.g., "#/components/schemas/ModelName")
        """
        for path in paths:
            if path in openapi_schema.get("paths", {}) and "post" in openapi_schema["paths"][path]:
                # Get the actual schema to extract ALL field definitions
                schema_name = schema_ref.split("/")[-1]  # Extract "ProxyChatCompletionRequest" from the ref
                actual_schema = openapi_schema.get("components", {}).get("schemas", {}).get(schema_name, {})
                schema_properties = actual_schema.get("properties", {})
                required_fields = actual_schema.get("required", [])
                
                # Extract $defs and add them to components/schemas
                # This fixes Pydantic v2 $defs not being resolvable in Swagger/OpenAPI
                if "$defs" in actual_schema:
                    CustomOpenAPISpec._move_defs_to_components(openapi_schema, actual_schema["$defs"])
                
                # Create an expanded inline schema instead of just a $ref
                # This makes Swagger UI show all individual fields in the request body editor
                expanded_schema = {
                    "type": "object",
                    "required": required_fields,
                    "properties": {}
                }
                
                # Add all properties with their full definitions
                for field_name, field_def in schema_properties.items():
                    expanded_field = CustomOpenAPISpec._expand_field_definition(field_def)
                    
                    # Rewrite $defs references to use components/schemas instead
                    expanded_field = CustomOpenAPISpec._rewrite_defs_refs(expanded_field)
                    
                    # Add a simple example for the messages field
                    if field_name == "messages":
                        expanded_field["example"] = [
                            {"role": "user", "content": "Hello, how are you?"}
                        ]
                    
                    expanded_schema["properties"][field_name] = expanded_field
                
                # Set the request body with the expanded schema
                openapi_schema["paths"][path]["post"]["requestBody"] = {
                    "required": True,
                    "content": {
                        "application/json": {
                            "schema": expanded_schema
                        }
                    }
                }
                
                # Keep any existing parameters (like path parameters) but remove conflicting query params
                if "parameters" in openapi_schema["paths"][path]["post"]:
                    existing_params = openapi_schema["paths"][path]["post"]["parameters"]
                    # Only keep path parameters, remove query params that conflict with request body
                    filtered_params = [
                        param for param in existing_params 
                        if param.get("in") == "path"
                    ]
                    openapi_schema["paths"][path]["post"]["parameters"] = filtered_params
    
    @staticmethod
    def _move_defs_to_components(openapi_schema: Dict[str, Any], defs: Dict[str, Any]) -> None:
        """
        Move $defs from Pydantic v2 schema to OpenAPI components/schemas.
        This makes the definitions resolvable in Swagger/OpenAPI viewers.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            defs: The $defs dictionary from Pydantic schema
        """
        if not defs:
            return
        
        # Ensure components/schemas exists
        if "components" not in openapi_schema:
            openapi_schema["components"] = {}
        if "schemas" not in openapi_schema["components"]:
            openapi_schema["components"]["schemas"] = {}
        
        # Add each definition to components/schemas
        for def_name, def_schema in defs.items():
            # Recursively rewrite any nested $defs references within this definition
            rewritten_def = CustomOpenAPISpec._rewrite_defs_refs(def_schema)
            openapi_schema["components"]["schemas"][def_name] = rewritten_def
            
            # If this definition also has $defs, process them recursively
            if "$defs" in def_schema:
                CustomOpenAPISpec._move_defs_to_components(openapi_schema, def_schema["$defs"])
    
    @staticmethod
    def _rewrite_defs_refs(schema: Any) -> Any:
        """
        Recursively rewrite $ref values from #/$defs/... to #/components/schemas/...
        This converts Pydantic v2 references to OpenAPI-compatible references.
        
        Args:
            schema: Schema object to process (can be dict, list, or primitive)
            
        Returns:
            Schema with rewritten references
        """
        if isinstance(schema, dict):
            result = {}
            for key, value in schema.items():
                if key == "$ref" and isinstance(value, str) and value.startswith("#/$defs/"):
                    # Rewrite the reference to use components/schemas
                    def_name = value.replace("#/$defs/", "")
                    result[key] = f"#/components/schemas/{def_name}"
                elif key == "$defs":
                    # Remove $defs from the schema since they're moved to components
                    continue
                else:
                    # Recursively process nested structures
                    result[key] = CustomOpenAPISpec._rewrite_defs_refs(value)
            return result
        elif isinstance(schema, list):
            return [CustomOpenAPISpec._rewrite_defs_refs(item) for item in schema]
        else:
            return schema
    
    @staticmethod
    def _extract_field_schema(field_def: Dict[str, Any]) -> Dict[str, Any]:
        """
        Extract a simple schema from a Pydantic field definition for parameter display.
        
        Args:
            field_def: Pydantic field definition
            
        Returns:
            Simplified schema for OpenAPI parameter
        """
        # Handle simple types
        if "type" in field_def:
            return {"type": field_def["type"]}
        
        # Handle anyOf (Optional fields in Pydantic v2)
        if "anyOf" in field_def:
            any_of = field_def["anyOf"]
            # Find the non-null type
            for option in any_of:
                if option.get("type") != "null":
                    return option
            # Fallback to string if all else fails
            return {"type": "string"}
        
        # Default fallback
        return {"type": "string"}
    
    @staticmethod
    def _expand_field_definition(field_def: Dict[str, Any]) -> Dict[str, Any]:
        """
        Expand a Pydantic field definition for inline use in OpenAPI schema.
        This creates a full field definition that Swagger UI can render as individual form fields.
        
        Args:
            field_def: Pydantic field definition
            
        Returns:
            Expanded field definition for OpenAPI schema
        """
        # Return the field definition as-is since Pydantic already provides proper schemas
        return field_def.copy()
    
    @staticmethod
    def add_request_schema(
        openapi_schema: Dict[str, Any], 
        model_class: Type, 
        schema_name: str, 
        paths: List[str],
        operation_name: str
    ) -> Dict[str, Any]:
        """
        Generic method to add a request schema to OpenAPI specification.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            model_class: The Pydantic model class to get schema from
            schema_name: Name for the schema component
            paths: List of paths to add the request body to
            operation_name: Name of the operation for logging (e.g., "chat completion", "embedding")
            
        Returns:
            Modified OpenAPI schema
        """
        try:
            # Get the schema for the model class
            request_schema = CustomOpenAPISpec.get_pydantic_schema(model_class)
            
            # Only proceed if we successfully got the schema
            if request_schema is not None:
                # Add schema to components
                CustomOpenAPISpec.add_schema_to_components(openapi_schema, schema_name, request_schema)
                
                # Add request body to specified endpoints
                CustomOpenAPISpec.add_request_body_to_paths(
                    openapi_schema, 
                    paths, 
                    f"#/components/schemas/{schema_name}"
                )
                
                verbose_proxy_logger.debug(f"Successfully added {schema_name} schema to OpenAPI spec")
            else:
                verbose_proxy_logger.debug(f"Could not get schema for {schema_name}")
                
        except Exception as e:
            # If schema addition fails, continue without it
            verbose_proxy_logger.debug(f"Failed to add {operation_name} request schema: {str(e)}")
        
        return openapi_schema
    
    @staticmethod
    def add_chat_completion_request_schema(openapi_schema: Dict[str, Any]) -> Dict[str, Any]:
        """
        Add ProxyChatCompletionRequest schema to chat completion endpoints for documentation.
        This shows the request body in Swagger without runtime validation.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            
        Returns:
            Modified OpenAPI schema
        """
        try:
            from litellm.proxy._types import ProxyChatCompletionRequest
            
            return CustomOpenAPISpec.add_request_schema(
                openapi_schema=openapi_schema,
                model_class=ProxyChatCompletionRequest,
                schema_name="ProxyChatCompletionRequest",
                paths=CustomOpenAPISpec.CHAT_COMPLETION_PATHS,
                operation_name="chat completion"
            )
        except ImportError as e:
            verbose_proxy_logger.debug(f"Failed to import ProxyChatCompletionRequest: {str(e)}")
            return openapi_schema
    
    @staticmethod
    def add_embedding_request_schema(openapi_schema: Dict[str, Any]) -> Dict[str, Any]:
        """
        Add EmbeddingRequest schema to embedding endpoints for documentation.
        This shows the request body in Swagger without runtime validation.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            
        Returns:
            Modified OpenAPI schema
        """
        try:
            from litellm.types.embedding import EmbeddingRequest
            
            return CustomOpenAPISpec.add_request_schema(
                openapi_schema=openapi_schema,
                model_class=EmbeddingRequest,
                schema_name="EmbeddingRequest",
                paths=CustomOpenAPISpec.EMBEDDING_PATHS,
                operation_name="embedding"
            )
        except ImportError as e:
            verbose_proxy_logger.debug(f"Failed to import EmbeddingRequest: {str(e)}")
            return openapi_schema
    
    @staticmethod
    def add_responses_api_request_schema(openapi_schema: Dict[str, Any]) -> Dict[str, Any]:
        """
        Add ResponsesAPIRequestParams schema to responses API endpoints for documentation.
        This shows the request body in Swagger without runtime validation.
        
        Args:
            openapi_schema: The OpenAPI schema dict to modify
            
        Returns:
            Modified OpenAPI schema
        """
        try:
            from litellm.types.llms.openai import ResponsesAPIRequestParams
            
            return CustomOpenAPISpec.add_request_schema(
                openapi_schema=openapi_schema,
                model_class=ResponsesAPIRequestParams,
                schema_name="ResponsesAPIRequestParams",
                paths=CustomOpenAPISpec.RESPONSES_API_PATHS,
                operation_name="responses API"
            )
        except ImportError as e:
            verbose_proxy_logger.debug(f"Failed to import ResponsesAPIRequestParams: {str(e)}")
            return openapi_schema
    
    @staticmethod
    def add_llm_api_request_schema_body(openapi_schema: Dict[str, Any]) -> Dict[str, Any]:
        """
        Add LLM API request schema bodies to OpenAPI specification for documentation.
        
        Args:
            openapi_schema: The base OpenAPI schema
            
        Returns:
            OpenAPI schema with added request body schemas
        """
        # Add chat completion request schema
        openapi_schema = CustomOpenAPISpec.add_chat_completion_request_schema(openapi_schema)
        
        # Add embedding request schema
        openapi_schema = CustomOpenAPISpec.add_embedding_request_schema(openapi_schema)
        
        # Add responses API request schema
        openapi_schema = CustomOpenAPISpec.add_responses_api_request_schema(openapi_schema)
        
        return openapi_schema 