"""Data models for OpenAPI spec parsing and MCP tool generation.

Supports OpenAPI 3.0+ and Swagger 2.0 specifications. Extracts the
information needed to generate MCP tools from REST API endpoints:
  - Operation metadata (method, path, summary, description)
  - Parameters (path, query, header, cookie)
  - Request body schemas
  - Authentication schemes
  - Response schemas (for description)
"""

from __future__ import annotations

from enum import StrEnum
from typing import Any

from pydantic import BaseModel, Field


class AuthSchemeType(StrEnum):
    """Supported authentication scheme types."""
    API_KEY = "apiKey"
    BEARER = "bearer"
    BASIC = "basic"
    OAUTH2 = "oauth2"
    NONE = "none"


class ApiKeyLocation(StrEnum):
    """Where API key is sent."""
    HEADER = "header"
    QUERY = "query"
    COOKIE = "cookie"


class AuthScheme(BaseModel):
    """An authentication scheme extracted from the OpenAPI spec."""
    name: str = Field(description="Security scheme name from the spec")
    type: AuthSchemeType = Field(description="Auth type")
    # API Key specific
    api_key_name: str | None = Field(default=None, description="Header/query param name for API key")
    api_key_location: ApiKeyLocation | None = Field(default=None, description="Where to send the API key")
    # Bearer specific
    bearer_format: str | None = Field(default=None, description="Bearer token format (e.g., 'JWT')")
    # OAuth2 specific
    oauth2_flows: dict[str, Any] | None = Field(default=None, description="OAuth2 flow configurations")
    # User-provided value
    value: str | None = Field(default=None, description="Configured credential value (memory only)")


class ParameterLocation(StrEnum):
    """Where a parameter is located in the HTTP request."""
    PATH = "path"
    QUERY = "query"
    HEADER = "header"
    COOKIE = "cookie"


class ApiParameter(BaseModel):
    """A single parameter for an API operation."""
    name: str = Field(description="Parameter name")
    location: ParameterLocation = Field(description="Where in the request")
    description: str = Field(default="", description="Human-readable description")
    required: bool = Field(default=False, description="Whether this parameter is required")
    schema_type: str = Field(default="string", description="JSON Schema type (string, integer, boolean, etc.)")
    default: Any = Field(default=None, description="Default value")
    enum_values: list[str] | None = Field(default=None, description="Allowed values for enum types")
    example: Any = Field(default=None, description="Example value from the spec")


class RequestBody(BaseModel):
    """Request body schema for an API operation."""
    content_type: str = Field(default="application/json", description="Content type")
    required: bool = Field(default=False)
    schema_properties: dict[str, dict[str, Any]] = Field(
        default_factory=dict,
        description="Property name → {type, description, required, ...}"
    )
    required_properties: list[str] = Field(default_factory=list)
    description: str = Field(default="")
    example: Any = Field(default=None)


class ApiOperation(BaseModel):
    """A single API operation (endpoint + method) from the OpenAPI spec.

    This is the primary unit that gets converted into an MCP tool.
    """
    operation_id: str = Field(description="Unique operation identifier (from spec or generated)")
    method: str = Field(description="HTTP method (get, post, put, patch, delete)")
    path: str = Field(description="URL path with {param} placeholders")
    summary: str = Field(default="", description="Short summary from the spec")
    description: str = Field(default="", description="Detailed description from the spec")
    tags: list[str] = Field(default_factory=list, description="Tags for categorization")
    parameters: list[ApiParameter] = Field(default_factory=list, description="Path, query, header params")
    request_body: RequestBody | None = Field(default=None, description="Request body if applicable")
    response_description: str = Field(default="", description="Description of the success response")
    deprecated: bool = Field(default=False)
    security: list[str] = Field(default_factory=list, description="Required security scheme names")

    # MCP tool customization (user can override)
    tool_name: str | None = Field(default=None, description="Custom MCP tool name (overrides operation_id)")
    tool_description: str | None = Field(default=None, description="Custom MCP tool description")
    enabled: bool = Field(default=True, description="Whether to expose this as an MCP tool")

    @property
    def effective_tool_name(self) -> str:
        """Get the tool name to use (custom or auto-generated)."""
        if self.tool_name:
            return self.tool_name
        if self.operation_id:
            return self.operation_id
        # Generate from method + path
        clean_path = self.path.strip("/").replace("/", "_").replace("{", "").replace("}", "")
        return f"{self.method}_{clean_path}"

    @property
    def effective_description(self) -> str:
        """Get the description to use (custom or from spec)."""
        if self.tool_description:
            return self.tool_description
        parts = []
        if self.summary:
            parts.append(self.summary)
        if self.description and self.description != self.summary:
            parts.append(self.description)
        if not parts:
            parts.append(f"{self.method.upper()} {self.path}")
        return " ".join(parts)

    def to_json_schema(self) -> dict[str, Any]:
        """Generate JSON Schema for this operation's input parameters.

        This becomes the MCP tool's input_schema.
        """
        properties: dict[str, Any] = {}
        required: list[str] = []

        for param in self.parameters:
            prop: dict[str, Any] = {"type": param.schema_type}
            if param.description:
                prop["description"] = param.description
            if param.default is not None:
                prop["default"] = param.default
            if param.enum_values:
                prop["enum"] = param.enum_values
            if param.example is not None:
                prop["examples"] = [param.example]
            properties[param.name] = prop
            if param.required:
                required.append(param.name)

        # Request body properties
        if self.request_body:
            for prop_name, prop_schema in self.request_body.schema_properties.items():
                prop_entry: dict[str, Any] = {"type": prop_schema.get("type", "string")}
                if prop_schema.get("description"):
                    prop_entry["description"] = prop_schema["description"]
                if prop_schema.get("enum"):
                    prop_entry["enum"] = prop_schema["enum"]
                if prop_schema.get("example") is not None:
                    prop_entry["examples"] = [prop_schema["example"]]
                properties[prop_name] = prop_entry

            for rp in self.request_body.required_properties:
                if rp not in required:
                    required.append(rp)

        schema: dict[str, Any] = {"type": "object", "properties": properties}
        if required:
            schema["required"] = required
        return schema


class ApiSpec(BaseModel):
    """A parsed OpenAPI/Swagger specification ready for MCP tool generation."""
    title: str = Field(description="API title from the spec")
    description: str = Field(default="", description="API description")
    version: str = Field(default="", description="API version")
    base_url: str = Field(default="", description="Base URL for all endpoints")
    operations: list[ApiOperation] = Field(default_factory=list, description="All discovered operations")
    auth_schemes: list[AuthScheme] = Field(default_factory=list, description="Available auth schemes")

    # User customization
    plugin_name: str | None = Field(default=None, description="Custom name for the generated MCP plugin")
    plugin_description: str | None = Field(default=None, description="Custom description for the plugin")

    @property
    def enabled_operations(self) -> list[ApiOperation]:
        """Get only the enabled (non-deprecated) operations."""
        return [op for op in self.operations if op.enabled and not op.deprecated]

    @property
    def operation_count(self) -> int:
        return len(self.operations)

    @property
    def enabled_count(self) -> int:
        return len(self.enabled_operations)

    @property
    def tags(self) -> list[str]:
        """Get unique tags across all operations."""
        seen: set[str] = set()
        result: list[str] = []
        for op in self.operations:
            for tag in op.tags:
                if tag not in seen:
                    seen.add(tag)
                    result.append(tag)
        return result
