"""
ByteDocs FastAPI - Route Analyzer
Auto-detect and analyze FastAPI routes
"""

import inspect
from typing import Any, Callable, Dict, List, Optional, Union, get_type_hints, get_origin, get_args
from fastapi import FastAPI, APIRouter
from fastapi.routing import APIRoute
from pydantic import BaseModel
from pydantic.fields import FieldInfo

from ..core.types import (
    RouteInfo,
    Parameter,
    ParameterLocation,
    RequestBody,
    ResponseDef,
    Schema,
)
from .ast_analyzer import enhance_schema_with_ast, get_handler_source_code


def extract_routes(app: FastAPI, exclude_paths: List[str] = None) -> List[RouteInfo]:
    """Extract all routes from FastAPI application"""
    if exclude_paths is None:
        exclude_paths = []

    routes = []

    for route in app.routes:
        if isinstance(route, APIRoute):
            path = route.path

            # Check if should be excluded
            if should_exclude_path(path, exclude_paths):
                continue

            # Get methods for this route
            methods = route.methods or {"GET"}

            for method in methods:
                route_info = analyze_route(route, method, path)
                if route_info:
                    routes.append(route_info)

    print(f"[ByteDocs] Detected {len(routes)} routes")
    return routes


def should_exclude_path(path: str, exclude_paths: List[str]) -> bool:
    """Check if path should be excluded"""
    for exclude_pattern in exclude_paths:
        if path.startswith(exclude_pattern):
            return True
    return False


def analyze_route(route: APIRoute, method: str, path: str) -> Optional[RouteInfo]:
    """Analyze a single route and extract metadata"""
    try:
        handler = route.endpoint

        # Get basic info from route
        summary = route.summary or generate_summary(method, path)
        description = route.description
        tags = list(route.tags) if route.tags else infer_tags(path)

        # Extract parameters
        parameters = extract_parameters(route, handler)

        # Extract request body
        request_body = extract_request_body(route, handler, method)

        # Extract responses
        responses = extract_responses(route, handler, method)

        return RouteInfo(
            method=method.upper(),
            path=path,
            handler=handler,
            summary=summary,
            description=description,
            parameters=parameters,
            request_body=request_body,
            responses=responses,
            tags=tags,
        )
    except Exception as e:
        print(f"[ByteDocs] Error analyzing route {method} {path}: {e}")
        return None


def extract_parameters(route: APIRoute, handler: Callable) -> List[Parameter]:
    """Extract parameters from route"""
    parameters = []

    # Get function signature
    sig = inspect.signature(handler)
    type_hints = get_type_hints(handler)

    for param_name, param in sig.parameters.items():
        # Skip special parameters
        if param_name in ["self", "cls", "request", "response"]:
            continue

        # Determine parameter location and schema
        param_location = None
        schema = None
        required = param.default == inspect.Parameter.empty
        description = None

        # Check if it's a path parameter
        if f"{{{param_name}}}" in route.path:
            param_location = ParameterLocation.PATH
            required = True

        # Get type annotation
        param_type = type_hints.get(param_name, param.annotation)

        # Handle Query, Path, Header, Cookie from FastAPI
        if hasattr(param.default, "__class__"):
            default_class = param.default.__class__.__name__

            if default_class == "QueryInfo":
                param_location = ParameterLocation.QUERY
            elif default_class == "PathInfo":
                param_location = ParameterLocation.PATH
            elif default_class == "HeaderInfo":
                param_location = ParameterLocation.HEADER
            elif default_class == "CookieInfo":
                param_location = ParameterLocation.COOKIE

            # Get description from FieldInfo
            if isinstance(param.default, FieldInfo):
                description = param.default.description
                # Only set required=False if it has a default AND is not a path parameter
                # Path parameters MUST always be required per OpenAPI spec
                if param.default.default is not None and param.default.default != ...:
                    if param_location != ParameterLocation.PATH:
                        required = False

        # If location not determined, assume query
        if param_location is None:
            param_location = ParameterLocation.QUERY

        # Convert type to schema
        # Extract actual default value from FieldInfo if present
        actual_default = None
        if isinstance(param.default, FieldInfo):
            if param.default.default is not None and param.default.default != ...:
                actual_default = param.default.default
        elif param.default != inspect.Parameter.empty:
            # For non-FieldInfo defaults, check if it's JSON serializable
            try:
                import json
                json.dumps(param.default)
                actual_default = param.default
            except (TypeError, ValueError):
                actual_default = None

        schema = type_to_schema(param_type, actual_default)

        parameters.append(
            Parameter(
                name=param_name,
                location=param_location,
                schema=schema,
                description=description,
                required=required,
            )
        )

    return parameters


def extract_request_body(
    route: APIRoute, handler: Callable, method: str
) -> Optional[RequestBody]:
    """Extract request body schema from route"""
    # Only for methods that typically have body
    if method.upper() not in ["POST", "PUT", "PATCH"]:
        return None

    # Check if route has body field
    if hasattr(route, "body_field") and route.body_field:
        body_field = route.body_field
        schema = pydantic_model_to_schema(body_field.type_)

        return RequestBody(
            description="Request body",
            required=True,
            content={
                "application/json": {
                    "schema": schema,
                }
            },
        )

    # Fallback: Check function signature for Pydantic models
    sig = inspect.signature(handler)
    type_hints = get_type_hints(handler)

    for param_name, param in sig.parameters.items():
        # Skip special parameters
        if param_name in ["self", "cls", "request", "response"]:
            continue

        # Get type annotation
        param_type = type_hints.get(param_name, param.annotation)

        # Check if it's a Pydantic model (not a simple type or FastAPI dependency)
        if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
            # Check if it's not a query/path/header param
            if not hasattr(param.default, "__class__") or param.default.__class__.__name__ not in ["QueryInfo", "PathInfo", "HeaderInfo", "CookieInfo"]:
                # This is likely the request body
                schema = pydantic_model_to_schema(param_type)

                return RequestBody(
                    description=f"Request body ({param_type.__name__})",
                    required=param.default == inspect.Parameter.empty,
                    content={
                        "application/json": {
                            "schema": schema,
                        }
                    },
                )

    return None


def extract_responses(
    route: APIRoute, handler: Callable, method: str
) -> Dict[str, ResponseDef]:
    """Extract response schemas from route"""
    responses = {}

    # Get responses from route.responses (OpenAPI responses)
    if hasattr(route, "responses") and route.responses:
        for status_code, response_data in route.responses.items():
            status_str = str(status_code)
            if isinstance(response_data, dict):
                description = response_data.get("description", get_status_description(int(status_code)))
                responses[status_str] = ResponseDef(description=description)
            else:
                responses[status_str] = ResponseDef(
                    description=get_status_description(int(status_code))
                )

    # Get response model from route
    if hasattr(route, "response_model") and route.response_model:
        default_status = "201" if method.upper() == "POST" else "200"

        # Convert response model to schema
        schema = pydantic_model_to_schema(route.response_model)

        responses[default_status] = ResponseDef(
            description=get_status_description(int(default_status)),
            content={
                "application/json": {
                    "schema": schema,
                }
            },
        )

    # If no responses defined, add default
    if not responses:
        default_status = "201" if method.upper() == "POST" else "200"
        responses[default_status] = ResponseDef(
            description=get_status_description(int(default_status)),
            content={
                "application/json": {
                    "schema": Schema(type="object"),
                }
            },
        )

    # Enhance with AST analysis
    try:
        source_code = get_handler_source_code(handler)
        if source_code:
            # Convert ResponseDef to dict for AST enhancement
            responses_dict = {}
            for status, resp_def in responses.items():
                resp_dict = {"description": resp_def.description}
                if resp_def.content:
                    resp_dict["content"] = {}
                    for content_type, content_data in resp_def.content.items():
                        resp_dict["content"][content_type] = {
                            "schema": dataclass_to_dict(content_data.get("schema"))
                            if "schema" in content_data
                            else {}
                        }
                responses_dict[status] = resp_dict

            # Enhance with AST
            enhanced = enhance_schema_with_ast(responses_dict, handler, source_code)

            # Convert back to ResponseDef
            for status, resp_data in enhanced.items():
                if status in responses:
                    # Update existing response with enhanced schema
                    if "content" in resp_data:
                        content_dict = {}
                        for ct, ct_data in resp_data["content"].items():
                            content_dict[ct] = {
                                "schema": dict_to_schema(ct_data.get("schema", {}))
                            }
                        responses[status].content = content_dict
    except Exception as e:
        # AST analysis failed, continue with existing responses
        pass

    return responses


def pydantic_model_to_schema(model: Any) -> Schema:
    """Convert Pydantic model to OpenAPI schema"""
    # Handle generic types
    origin = get_origin(model)

    # Handle List[Model]
    if origin is list:
        args = get_args(model)
        if args:
            item_schema = pydantic_model_to_schema(args[0])
            return Schema(type="array", items=item_schema)
        return Schema(type="array", items=Schema(type="object"))

    # Handle Dict
    if origin is dict:
        return Schema(type="object")

    # Handle Optional
    if origin is type(None) or (hasattr(model, "__origin__") and type(None) in get_args(model)):
        args = get_args(model)
        if args:
            non_none_args = [arg for arg in args if arg is not type(None)]
            if non_none_args:
                schema = pydantic_model_to_schema(non_none_args[0])
                schema.nullable = True
                return schema

    # Check if it's a Pydantic model
    if inspect.isclass(model) and issubclass(model, BaseModel):
        properties = {}
        required = []

        # Get model schema
        if hasattr(model, "model_fields"):  # Pydantic v2
            fields = model.model_fields
            for field_name, field_info in fields.items():
                field_schema = type_to_schema(field_info.annotation, None)
                if field_info.description:
                    field_schema.description = field_info.description
                properties[field_name] = field_schema

                if field_info.is_required():
                    required.append(field_name)

        elif hasattr(model, "__fields__"):  # Pydantic v1
            fields = model.__fields__
            for field_name, field_info in fields.items():
                field_schema = type_to_schema(field_info.type_, field_info.default)
                if field_info.field_info.description:
                    field_schema.description = field_info.field_info.description
                properties[field_name] = field_schema

                if field_info.required:
                    required.append(field_name)

        return Schema(
            type="object",
            properties=properties,
            required=required if required else None,
        )

    # Fallback to simple type conversion
    return type_to_schema(model, None)


def type_to_schema(type_annotation: Any, default: Any = None) -> Schema:
    """Convert Python type annotation to OpenAPI schema"""
    # Handle None type
    if type_annotation is None or type_annotation is type(None):
        return Schema(type="null")

    # Get origin for generic types
    origin = get_origin(type_annotation)

    # Handle Optional[X] which is Union[X, None]
    # Check for Union and if None is in args
    if origin is Union:
        args = get_args(type_annotation)
        # Check if None/NoneType is in the union
        non_none_args = [arg for arg in args if arg is not type(None)]
        if len(non_none_args) < len(args):  # There was a None in the union
            if non_none_args:
                # This is Optional[X], extract X and mark as nullable
                schema = type_to_schema(non_none_args[0], default)
                schema.nullable = True
                return schema
        # If no None in union, just process first type
        if non_none_args:
            return type_to_schema(non_none_args[0], default)

    # Handle List, list
    if origin in (list, List):
        args = get_args(type_annotation)
        items = type_to_schema(args[0], None) if args else Schema(type="object")
        return Schema(type="array", items=items)

    # Handle Dict, dict
    if origin in (dict, Dict):
        return Schema(type="object")

    # Basic type mapping
    type_map = {
        str: Schema(type="string"),
        int: Schema(type="integer"),
        float: Schema(type="number"),
        bool: Schema(type="boolean"),
        bytes: Schema(type="string", format="binary"),
    }

    # Check if it's a basic type
    if type_annotation in type_map:
        schema = type_map[type_annotation]
        # Only set default if it's a JSON-serializable value
        if default is not None and default != inspect.Parameter.empty:
            try:
                import json
                json.dumps(default)
                schema.default = default
            except (TypeError, ValueError):
                # Default is not JSON serializable, skip it
                pass
        return schema

    # Handle string types
    if type_annotation == "str" or (isinstance(type_annotation, type) and issubclass(type_annotation, str)):
        return Schema(type="string")

    # Pydantic model
    if inspect.isclass(type_annotation) and issubclass(type_annotation, BaseModel):
        return pydantic_model_to_schema(type_annotation)

    # Fallback
    return Schema(type="object")


def generate_summary(method: str, path: str) -> str:
    """Generate summary from method and path"""
    method_upper = method.upper()
    path_parts = [p for p in path.split("/") if p and not p.startswith("{")]
    resource = path_parts[-1] if path_parts else "resource"

    resource_name = resource.replace("-", " ").replace("_", " ").strip()

    summary_map = {
        "GET": f"Get {resource_name}" if "{" in path else f"List {resource_name}",
        "POST": f"Create {resource_name}",
        "PUT": f"Update {resource_name}",
        "PATCH": f"Partially update {resource_name}",
        "DELETE": f"Delete {resource_name}",
    }

    return summary_map.get(method_upper, f"{method_upper} {resource_name}")


def infer_tags(path: str) -> List[str]:
    """Infer tags from path"""
    parts = [p for p in path.split("/") if p and not p.startswith("{")]
    if parts:
        tag = parts[0].replace("-", " ").replace("_", " ").title()
        return [tag]
    return ["Default"]


def get_status_description(status: int) -> str:
    """Get HTTP status description"""
    descriptions = {
        200: "Successful operation",
        201: "Resource created successfully",
        204: "No content",
        400: "Bad request",
        401: "Unauthorized",
        403: "Forbidden",
        404: "Resource not found",
        422: "Validation error",
        500: "Internal server error",
    }
    return descriptions.get(status, f"HTTP {status}")


def dataclass_to_dict(obj: Any) -> Dict[str, Any]:
    """Convert dataclass Schema to dict"""
    if hasattr(obj, "__dataclass_fields__"):
        result = {}
        for field_name in obj.__dataclass_fields__:
            value = getattr(obj, field_name)
            if value is not None:
                if hasattr(value, "__dataclass_fields__"):
                    result[field_name] = dataclass_to_dict(value)
                elif isinstance(value, dict):
                    result[field_name] = {k: dataclass_to_dict(v) if hasattr(v, "__dataclass_fields__") else v for k, v in value.items()}
                elif isinstance(value, list):
                    result[field_name] = [dataclass_to_dict(item) if hasattr(item, "__dataclass_fields__") else item for item in value]
                else:
                    result[field_name] = value
        return result
    return obj


def dict_to_schema(data: Dict[str, Any]) -> Schema:
    """Convert dict to Schema dataclass"""
    schema_args = {}
    for key, value in data.items():
        if key in Schema.__dataclass_fields__:
            if key == "properties" and isinstance(value, dict):
                # Convert nested properties
                schema_args[key] = {k: dict_to_schema(v) if isinstance(v, dict) else v for k, v in value.items()}
            elif key == "items" and isinstance(value, dict):
                schema_args[key] = dict_to_schema(value)
            else:
                schema_args[key] = value
    return Schema(**schema_args) if schema_args else Schema(type="object")


def convert_path_to_openapi(path: str) -> str:
    """Convert FastAPI path format to OpenAPI format (they're the same)"""
    return path
