"""
FastAPI integration for ActingWeb applications.

Automatically generates FastAPI routes and handles request/response transformation
with async support.
"""

import asyncio
import base64
import concurrent.futures
import inspect
import json
import logging
from typing import TYPE_CHECKING, Any

from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field

from ...aw_web_request import AWWebObj
from ...handlers import bot, factory, mcp, services
from .base_integration import BaseActingWebIntegration

if TYPE_CHECKING:
    from ..app import ActingWebApp

logger = logging.getLogger(__name__)


# Pydantic Models for Type Safety


class ActorCreateRequest(BaseModel):
    """Request model for creating a new actor."""

    creator: str = Field(..., description="Email or identifier of the actor creator")
    passphrase: str | None = Field(
        None, description="Optional passphrase for actor creation"
    )
    type: str | None = Field(
        None, description="Actor type, defaults to configured type"
    )
    desc: str | None = Field(None, description="Optional description for the actor")


class ActorResponse(BaseModel):
    """Response model for actor operations."""

    id: str = Field(..., description="Unique actor identifier")
    creator: str = Field(..., description="Email or identifier of the actor creator")
    url: str = Field(..., description="Full URL to the actor")
    type: str = Field(..., description="Actor type")
    desc: str | None = Field(None, description="Actor description")


class PropertyRequest(BaseModel):
    """Request model for property operations."""

    value: Any = Field(..., description="Property value (can be any JSON type)")
    protected: bool | None = Field(
        False, description="Whether this property is protected"
    )


class PropertyResponse(BaseModel):
    """Response model for property operations."""

    name: str = Field(..., description="Property name")
    value: Any = Field(..., description="Property value")
    protected: bool = Field(..., description="Whether this property is protected")


class TrustRequest(BaseModel):
    """Request model for trust relationship operations."""

    type: str = Field(..., description="Type of trust relationship")
    peerid: str = Field(..., description="Peer actor identifier")
    baseuri: str = Field(..., description="Base URI of the peer actor")
    desc: str | None = Field(
        None, description="Optional description of the relationship"
    )


class TrustResponse(BaseModel):
    """Response model for trust relationship operations."""

    type: str = Field(..., description="Type of trust relationship")
    peerid: str = Field(..., description="Peer actor identifier")
    baseuri: str = Field(..., description="Base URI of the peer actor")
    desc: str | None = Field(None, description="Description of the relationship")


class SubscriptionRequest(BaseModel):
    """Request model for subscription operations."""

    peerid: str = Field(..., description="Peer actor identifier")
    hook: str = Field(..., description="Hook URL to be called")
    granularity: str | None = Field("message", description="Subscription granularity")
    desc: str | None = Field(None, description="Optional description")


class SubscriptionResponse(BaseModel):
    """Response model for subscription operations."""

    id: str = Field(..., description="Subscription identifier")
    peerid: str = Field(..., description="Peer actor identifier")
    hook: str = Field(..., description="Hook URL")
    granularity: str = Field(..., description="Subscription granularity")
    desc: str | None = Field(None, description="Subscription description")


class CallbackRequest(BaseModel):
    """Request model for callback operations."""

    data: dict[str, Any] = Field(default_factory=dict, description="Callback data")


class CallbackResponse(BaseModel):
    """Response model for callback operations."""

    result: Any = Field(..., description="Callback execution result")
    success: bool = Field(..., description="Whether callback executed successfully")


class MethodRequest(BaseModel):
    """Request model for method calls."""

    data: dict[str, Any] = Field(default_factory=dict, description="Method parameters")


class MethodResponse(BaseModel):
    """Response model for method calls."""

    result: Any = Field(..., description="Method execution result")
    success: bool = Field(..., description="Whether method executed successfully")


class ActionRequest(BaseModel):
    """Request model for action triggers."""

    data: dict[str, Any] = Field(default_factory=dict, description="Action parameters")


class ActionResponse(BaseModel):
    """Response model for action triggers."""

    result: Any = Field(..., description="Action execution result")
    success: bool = Field(..., description="Whether action executed successfully")


class ErrorResponse(BaseModel):
    """Standard error response model."""

    error: str = Field(..., description="Error message")
    status_code: int = Field(..., description="HTTP status code")
    details: dict[str, Any] | None = Field(None, description="Additional error details")


# Dependency Injection Functions


async def get_actor_from_path(actor_id: str, request: Request) -> dict[str, Any] | None:
    """
    Dependency to extract and validate actor from path parameter.
    Returns actor data or None if not found.
    """
    # This would typically load the actor from database
    # For now, we'll return the actor_id for the handlers to process
    return {"id": actor_id, "request": request}


async def get_basic_auth(request: Request) -> dict[str, str] | None:
    """
    Dependency to extract basic authentication credentials.
    Returns auth data or None if not provided.
    """
    auth_header = request.headers.get("authorization")
    if not auth_header or not auth_header.startswith("Basic "):
        return None

    try:
        # Decode base64 auth string
        auth_data = base64.b64decode(auth_header[6:]).decode("utf-8")
        username, password = auth_data.split(":", 1)
        return {"username": username, "password": password}
    except (ValueError, UnicodeDecodeError):
        return None


async def get_bearer_token(request: Request) -> str | None:
    """
    Dependency to extract bearer token from Authorization header.
    Returns token string or None if not provided.
    """
    auth_header = request.headers.get("authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        return None
    return auth_header[7:]


async def authenticate_google_oauth(
    request: Request, config: Any
) -> tuple[Any, str] | None:
    """
    Authenticate Google OAuth2 Bearer token and return actor.

    Returns:
        Tuple of (actor, email) or None if authentication failed
    """
    bearer_token = await get_bearer_token(request)
    if not bearer_token:
        return None

    try:
        from ...oauth2 import create_oauth2_authenticator

        authenticator = create_oauth2_authenticator(config)
        result = authenticator.authenticate_bearer_token(bearer_token)
        if (
            result
            and len(result) == 2
            and result[0] is not None
            and result[1] is not None
        ):
            return result  # type: ignore
        return None
    except Exception as e:
        logger.error(f"OAuth2 authentication error: {e}")
        return None


def create_oauth_redirect_response(
    config: Any, redirect_after_auth: str = "", clear_cookie: bool = False
) -> RedirectResponse | Response:
    """
    Create OAuth2 redirect response to configured OAuth2 provider.

    Args:
        config: ActingWeb configuration
        redirect_after_auth: URL to redirect to after successful auth
        clear_cookie: Whether to clear expired oauth_token cookie

    Returns:
        RedirectResponse to configured OAuth2 provider
    """
    try:
        from ...oauth2 import create_oauth2_authenticator

        authenticator = create_oauth2_authenticator(config)
        if authenticator.is_enabled():
            auth_url = authenticator.create_authorization_url(
                redirect_after_auth=redirect_after_auth
            )
            if auth_url:
                redirect_response = RedirectResponse(url=auth_url, status_code=302)
                if clear_cookie:
                    # Clear the expired oauth_token cookie
                    redirect_response.delete_cookie("oauth_token", path="/")
                    logger.debug("Cleared expired oauth_token cookie")
                return redirect_response
    except Exception as e:
        logger.error(f"Error creating OAuth2 redirect: {e}")

    # Fallback to 401 if OAuth2 not configured
    response = Response(content="Authentication required", status_code=401)
    # Prefer dynamic header based on configured OAuth2 provider if available
    add_www_authenticate_header(response, config)
    return response


def add_www_authenticate_header(response: Response, config: Any) -> None:
    """
    Add WWW-Authenticate header for OAuth2 authentication.
    """
    try:
        from ...oauth2 import create_oauth2_authenticator

        authenticator = create_oauth2_authenticator(config)
        if authenticator.is_enabled():
            www_auth = authenticator.create_www_authenticate_header()
            response.headers["WWW-Authenticate"] = www_auth
    except Exception as e:
        logger.error(f"Error adding WWW-Authenticate header: {e}")
        response.headers["WWW-Authenticate"] = 'Bearer realm="ActingWeb"'


async def check_authentication_and_redirect(
    request: Request, config: Any
) -> RedirectResponse | None:
    """
    Check if request is authenticated, if not return OAuth2 redirect.

    Returns:
        RedirectResponse to Google OAuth2 if not authenticated, None if authenticated
    """
    # Check for Basic auth
    basic_auth = await get_basic_auth(request)
    if basic_auth:
        return None  # Has basic auth, let normal flow handle it

    # Check for Bearer token
    bearer_token = await get_bearer_token(request)
    if bearer_token:
        # If a Bearer token is present, let the underlying handlers verify it.
        # This supports both OAuth2 tokens and ActingWeb trust secret tokens
        # without forcing an OAuth2 redirect here.
        return None

    # Check for OAuth token cookie (for session-based authentication)
    oauth_cookie = request.cookies.get("oauth_token")
    if oauth_cookie:
        logger.debug(f"Found oauth_token cookie with length {len(oauth_cookie)}")

        # First, check if this is an ActingWeb session token (SPA or /www)
        try:
            from ...oauth_session import get_oauth2_session_manager

            session_manager = get_oauth2_session_manager(config)
            token_data = session_manager.validate_access_token(oauth_cookie)
            if token_data:
                actor_id = token_data.get("actor_id")
                logger.debug(
                    f"ActingWeb session token validation successful for actor {actor_id}"
                )
                return None  # Valid ActingWeb token
        except Exception as e:
            logger.debug(f"ActingWeb token validation failed: {e}")

        # Fall back to validating as OAuth provider token (legacy support)
        try:
            from ...oauth2 import create_oauth2_authenticator

            authenticator = create_oauth2_authenticator(config)
            if authenticator.is_enabled():
                user_info = authenticator.validate_token_and_get_user_info(oauth_cookie)
                if user_info:
                    email = authenticator.get_email_from_user_info(
                        user_info, oauth_cookie
                    )
                    if email:
                        logger.debug(f"OAuth cookie validation successful for {email}")
                        return None  # Valid OAuth cookie
                logger.debug(
                    "OAuth cookie token is expired or invalid - will redirect to fresh OAuth"
                )
                # Token expired/invalid - fall through to create redirect response with cookie cleanup
        except Exception as e:
            logger.debug(
                f"OAuth cookie validation error: {e} - will redirect to fresh OAuth"
            )
            # Validation failed - fall through to redirect

    # No valid authentication - redirect to OAuth2 provider
    original_url = str(request.url)
    # Clear cookie if we had an expired token
    clear_cookie = bool(oauth_cookie)
    result = create_oauth_redirect_response(
        config, redirect_after_auth=original_url, clear_cookie=clear_cookie
    )
    if isinstance(result, RedirectResponse):
        return result
    return None


async def validate_content_type(
    request: Request, expected: str = "application/json"
) -> bool:
    """
    Dependency to validate request content type.
    Returns True if content type matches expected type.
    """
    content_type = request.headers.get("content-type", "")
    return expected in content_type


async def get_json_body(request: Request) -> dict[str, Any]:
    """
    Dependency to parse JSON request body.
    Returns parsed JSON data or empty dict.
    """
    try:
        body = await request.body()
        if not body:
            return {}
        parsed_json = json.loads(body.decode("utf-8"))
        return parsed_json if isinstance(parsed_json, dict) else {}
    except (json.JSONDecodeError, UnicodeDecodeError):
        return {}


class FastAPIIntegration(BaseActingWebIntegration):
    """
    FastAPI integration for ActingWeb applications.

    Automatically sets up all ActingWeb routes and handles request/response
    transformation between FastAPI and ActingWeb with async support.
    """

    def __init__(
        self,
        aw_app: "ActingWebApp",
        fastapi_app: FastAPI,
        templates_dir: str | None = None,
    ):
        super().__init__(aw_app)
        self.fastapi_app = fastapi_app
        self.templates = (
            Jinja2Templates(directory=templates_dir) if templates_dir else None
        )
        self.logger = logging.getLogger(__name__)
        # Thread pool for running synchronous ActingWeb handlers
        self.executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=10, thread_name_prefix="aw-handler"
        )

    def _prefer_async_handlers(self) -> bool:
        """Override to indicate FastAPI should use async handlers.

        This tells the handler factory to create AsyncMethodsHandler and
        AsyncActionsHandler instead of the sync variants, enabling native
        async execution without thread pool overhead.

        Returns:
            True to use async handlers
        """
        return True

    def shutdown(self) -> None:
        """Shutdown the thread pool executor."""
        if hasattr(self, "executor"):
            self.executor.shutdown(wait=True)

    async def _check_auth_or_redirect(
        self, request: Request
    ) -> RedirectResponse | None:
        """Helper to check authentication and return redirect if needed."""
        return await check_authentication_and_redirect(
            request, self.aw_app.get_config()
        )

    def setup_routes(self) -> None:
        """Setup all ActingWeb routes in FastAPI."""

        # Root factory route
        @self.fastapi_app.get("/")
        async def app_root_get(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # GET requests don't require authentication - show email form
            return await self._handle_factory_get_request(request)

        @self.fastapi_app.post("/")
        async def app_root_post(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # Check if this is a JSON API request or web form request
            content_type = request.headers.get("content-type", "")
            accepts_json = (
                request.headers.get("accept", "").find("application/json") >= 0
            )
            is_json_request = "application/json" in content_type

            if is_json_request or accepts_json:
                # Handle JSON API requests with the standard factory handler
                return await self._handle_factory_request(request)
            else:
                # For web form requests, extract email and redirect to OAuth2 with email hint
                return await self._handle_factory_post_with_oauth_redirect(request)

        # Google OAuth callback
        @self.fastapi_app.get("/oauth/callback")
        async def oauth_callback_handler(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # Handle both Google OAuth2 callback (for ActingWeb) and MCP OAuth2 callback
            # Determine which flow based on state parameter
            state = request.query_params.get("state", "")
            code = request.query_params.get("code", "")
            error = request.query_params.get("error", "")

            # Check if this is an MCP OAuth2 callback (encrypted state)
            self.logger.debug(
                f"OAuth callback received - code: {bool(code)}, error: {error}, state: {state[:100]}..."
            )  # Log first 100 chars

            # Debug: Check if MCP is enabled
            config = self.aw_app.get_config()
            mcp_enabled = getattr(config, "mcp", False)
            self.logger.debug(f"MCP enabled in config: {mcp_enabled}")

            try:
                from ...oauth2_server.state_manager import get_oauth2_state_manager

                state_manager = get_oauth2_state_manager(self.aw_app.get_config())
                self.logger.debug("State manager created successfully")

                mcp_context = state_manager.extract_mcp_context(state)
                self.logger.debug(
                    f"MCP context extraction result: {mcp_context is not None}"
                )

                if mcp_context:
                    self.logger.debug(
                        f"Using MCP OAuth2 callback handler with context: {mcp_context}"
                    )
                    # This is an MCP OAuth2 callback
                    return await self._handle_oauth2_endpoint(request, "callback")
                else:
                    self.logger.debug(
                        "No MCP context found, using standard OAuth2 callback"
                    )
            except Exception:
                # Not an MCP callback or state manager not available
                self.logger.error("Error checking MCP context", exc_info=True)

            # Default to Google OAuth2 callback for ActingWeb
            self.logger.debug("Using standard Google OAuth2 callback handler")
            return await self._handle_google_oauth_callback(request)

        # OAuth2 email input - handles email collection when OAuth provider doesn't provide one
        @self.fastapi_app.get("/oauth/email")
        async def oauth_email_get(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_email(request)

        @self.fastapi_app.post("/oauth/email")
        async def oauth_email_post(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_email(request)

        # Email verification endpoint - verifies email addresses for OAuth2 actors
        @self.fastapi_app.get("/{actor_id}/www/verify_email")
        async def email_verification_get(request: Request, actor_id: str) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_email_verification(request, actor_id)

        @self.fastapi_app.post("/{actor_id}/www/verify_email")
        async def email_verification_post(request: Request, actor_id: str) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_email_verification(request, actor_id)

        # OAuth2 server endpoints for MCP clients
        @self.fastapi_app.post("/oauth/register")
        @self.fastapi_app.options("/oauth/register")
        async def oauth2_register(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_endpoint(request, "register")

        @self.fastapi_app.get("/oauth/authorize")
        @self.fastapi_app.options("/oauth/authorize")
        async def oauth2_authorize_get(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_endpoint(request, "authorize")

        @self.fastapi_app.post("/oauth/authorize")
        async def oauth2_authorize_post(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_endpoint(request, "authorize")

        @self.fastapi_app.post("/oauth/token")
        @self.fastapi_app.options("/oauth/token")
        async def oauth2_token(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_oauth2_endpoint(request, "token")

        @self.fastapi_app.get("/oauth/logout")
        @self.fastapi_app.post("/oauth/logout")
        @self.fastapi_app.options("/oauth/logout")
        async def oauth2_logout(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """
            Unified logout endpoint that handles both:
            1. MCP OAuth2 client token revocation (if Authorization header present)
            2. Web UI session logout (if oauth_token cookie present)

            Uses SPA CORS (echo origin + credentials) because logout clears cookies
            and cross-origin SPAs need credentialed CORS for Set-Cookie to work.
            """

            # Helper to get SPA CORS headers (echo origin + credentials)
            def get_spa_cors_headers() -> dict[str, str]:
                origin = request.headers.get("origin", "")
                return {
                    "Access-Control-Allow-Origin": origin if origin else "*",
                    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                    "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept",
                    "Access-Control-Allow-Credentials": "true",
                    "Access-Control-Max-Age": "86400",
                }

            # Handle OPTIONS (CORS preflight) immediately
            if request.method == "OPTIONS":
                return JSONResponse(
                    {"message": "CORS preflight"},
                    status_code=200,
                    headers=get_spa_cors_headers(),
                )

            # Check if this is an AJAX request (expects JSON response)
            content_type = request.headers.get("content-type", "")
            is_ajax = (
                "application/json" in content_type
                or request.headers.get("x-requested-with") == "XMLHttpRequest"
            )

            # First, handle MCP OAuth2 token revocation if Authorization header present
            auth_header = request.headers.get("authorization", "")
            if auth_header.startswith("Bearer "):
                self.logger.info("Logout: Revoking MCP OAuth2 token")
                await self._handle_oauth2_endpoint(request, "logout")

            # Check if this is a web UI logout (oauth_token cookie present)
            oauth_cookie = request.cookies.get("oauth_token")
            if oauth_cookie:
                self.logger.info("Logout: Clearing web UI session (oauth_token cookie)")

                if is_ajax:
                    # Return JSON response for AJAX requests
                    response = JSONResponse(
                        {
                            "success": True,
                            "message": "Logged out successfully",
                            "redirect_url": "/",
                        },
                        headers=get_spa_cors_headers(),
                    )
                    response.delete_cookie("oauth_token", path="/")
                    return response
                else:
                    # Return redirect for direct navigation
                    response = RedirectResponse(url="/", status_code=302)
                    response.delete_cookie("oauth_token", path="/")
                    # Add CORS headers even for redirects
                    for key, value in get_spa_cors_headers().items():
                        response.headers[key] = value
                    return response

            # If neither token nor cookie, just return success
            if not auth_header and not oauth_cookie:
                self.logger.info("Logout: No active session found")
                return JSONResponse(
                    {"message": "No active session to logout"},
                    status_code=200,
                    headers=get_spa_cors_headers(),
                )

            # MCP client logout without web UI redirect
            return await self._handle_oauth2_endpoint(request, "logout")

        # Unified OAuth endpoints (JSON API, accessible at /oauth/*)
        @self.fastapi_app.get("/oauth/config")
        @self.fastapi_app.options("/oauth/config")
        async def oauth2_config(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Get OAuth configuration."""
            return await self._handle_oauth2_spa_endpoint(request, "config")

        @self.fastapi_app.post("/oauth/revoke")
        @self.fastapi_app.options("/oauth/revoke")
        async def oauth2_revoke(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Revoke access and/or refresh tokens."""
            return await self._handle_oauth2_spa_endpoint(request, "revoke")

        @self.fastapi_app.get("/oauth/session")
        @self.fastapi_app.options("/oauth/session")
        async def oauth2_session(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Check session status."""
            return await self._handle_oauth2_spa_endpoint(request, "session")

        # SPA-specific OAuth endpoints (different purpose than MCP OAuth2)
        @self.fastapi_app.post("/oauth/spa/authorize")
        @self.fastapi_app.options("/oauth/spa/authorize")
        async def oauth2_spa_authorize(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Initiate external OAuth flow (ActingWeb as OAuth client)."""
            return await self._handle_oauth2_spa_endpoint(request, "authorize")

        @self.fastapi_app.post("/oauth/spa/token")
        @self.fastapi_app.options("/oauth/spa/token")
        async def oauth2_spa_token(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Token refresh with rotation for external provider tokens."""
            return await self._handle_oauth2_spa_endpoint(request, "token")

        # Backward compatibility routes
        @self.fastapi_app.get("/oauth/spa/config")
        @self.fastapi_app.options("/oauth/spa/config")
        async def oauth2_spa_config_compat(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Get OAuth configuration (deprecated, use /oauth/config)."""
            return await self._handle_oauth2_spa_endpoint(request, "config")

        @self.fastapi_app.get("/oauth/spa/callback")
        @self.fastapi_app.options("/oauth/spa/callback")
        async def oauth2_spa_callback(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Deprecated: Use /oauth/callback instead."""
            return await self._handle_oauth2_spa_endpoint(request, "callback")

        @self.fastapi_app.post("/oauth/spa/revoke")
        @self.fastapi_app.options("/oauth/spa/revoke")
        async def oauth2_spa_revoke_compat(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Revoke tokens (deprecated, use /oauth/revoke)."""
            return await self._handle_oauth2_spa_endpoint(request, "revoke")

        @self.fastapi_app.get("/oauth/spa/session")
        @self.fastapi_app.options("/oauth/spa/session")
        async def oauth2_spa_session_compat(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Check session (deprecated, use /oauth/session)."""
            return await self._handle_oauth2_spa_endpoint(request, "session")

        @self.fastapi_app.get("/oauth/spa/session/{session_id}")
        async def oauth2_spa_session_retrieve(
            request: Request, session_id: str
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Retrieve pending SPA session data after OAuth callback."""
            return await self._handle_spa_session_retrieve(request, session_id)

        @self.fastapi_app.post("/oauth/spa/logout")
        @self.fastapi_app.options("/oauth/spa/logout")
        async def oauth2_spa_logout(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            """Logout (deprecated, use /oauth/logout)."""
            # Delegate to main logout handler for consistency
            return await self._handle_oauth2_endpoint(request, "logout")

        # OAuth2 discovery endpoint - removed duplicate, handled by OAuth2EndpointsHandler below

        # Bot endpoint
        @self.fastapi_app.post("/bot")
        async def app_bot(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_bot_request(request)

        # MCP endpoint
        @self.fastapi_app.get("/mcp")
        @self.fastapi_app.post("/mcp")
        async def app_mcp(request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # For MCP, allow initial handshake without authentication
            # Authentication will be handled within the MCP protocol
            return await self._handle_mcp_request(request)

        # OAuth2 Discovery endpoints using OAuth2EndpointsHandler
        @self.fastapi_app.get("/.well-known/oauth-authorization-server")
        @self.fastapi_app.options("/.well-known/oauth-authorization-server")
        async def oauth_discovery(request: Request) -> JSONResponse:  # pyright: ignore[reportUnusedFunction]
            """OAuth2 Authorization Server Discovery endpoint (RFC 8414)."""
            return await self._handle_oauth2_discovery_endpoint(
                request, ".well-known/oauth-authorization-server"
            )

        @self.fastapi_app.get("/.well-known/oauth-protected-resource")
        @self.fastapi_app.options("/.well-known/oauth-protected-resource")
        async def oauth_protected_resource_discovery(request: Request) -> JSONResponse:  # pyright: ignore[reportUnusedFunction]
            """OAuth2 Protected Resource discovery endpoint."""
            return await self._handle_oauth2_discovery_endpoint(
                request, ".well-known/oauth-protected-resource"
            )

        @self.fastapi_app.get("/.well-known/oauth-protected-resource/mcp")
        @self.fastapi_app.options("/.well-known/oauth-protected-resource/mcp")
        async def oauth_protected_resource_mcp_discovery(
            request: Request,
        ) -> JSONResponse:  # pyright: ignore[reportUnusedFunction]
            """OAuth2 Protected Resource discovery endpoint for MCP."""
            return await self._handle_oauth2_discovery_endpoint(
                request, ".well-known/oauth-protected-resource/mcp"
            )

        # MCP information endpoint
        @self.fastapi_app.get("/mcp/info")
        async def mcp_info() -> dict[str, Any]:  # pyright: ignore[reportUnusedFunction]
            """MCP information endpoint."""
            return self._create_mcp_info_response()

        # Actor root
        @self.fastapi_app.get("/{actor_id}")
        @self.fastapi_app.post("/{actor_id}")
        @self.fastapi_app.delete("/{actor_id}")
        async def app_actor_root(actor_id: str, request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # For browser requests (Accept: text/html), redirect to /login if not authenticated
            # This provides a consistent login experience instead of going directly to OAuth
            accept_header = request.headers.get("accept", "")
            if "text/html" in accept_header:
                # Check if user is authenticated
                basic_auth = await get_basic_auth(request)
                bearer_token = await get_bearer_token(request)
                oauth_cookie = request.cookies.get("oauth_token")

                if not basic_auth and not bearer_token and not oauth_cookie:
                    # Unauthenticated browser - redirect to login page
                    config = self.aw_app.get_config()
                    return RedirectResponse(url=f"{config.root}login", status_code=302)

            # For API requests or authenticated browsers, use normal auth flow
            auth_redirect = await check_authentication_and_redirect(
                request, self.aw_app.get_config()
            )
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(request, actor_id, "root")

        # Actor meta
        @self.fastapi_app.get("/{actor_id}/meta")
        @self.fastapi_app.get("/{actor_id}/meta/{path:path}")
        async def app_meta(actor_id: str, request: Request, path: str = "") -> Response:  # pyright: ignore[reportUnusedFunction]
            # Meta endpoint should be public for peer discovery - no authentication required
            return await self._handle_actor_request(
                request, actor_id, "meta", path=path
            )

        # Actor www
        @self.fastapi_app.get("/{actor_id}/www")
        @self.fastapi_app.post("/{actor_id}/www")
        @self.fastapi_app.delete("/{actor_id}/www")
        @self.fastapi_app.get("/{actor_id}/www/{path:path}")
        @self.fastapi_app.post("/{actor_id}/www/{path:path}")
        @self.fastapi_app.delete("/{actor_id}/www/{path:path}")
        async def app_www(actor_id: str, request: Request, path: str = "") -> Response:  # pyright: ignore[reportUnusedFunction]
            # Check authentication and redirect to Google OAuth2 if needed
            auth_redirect = await self._check_auth_or_redirect(request)
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(request, actor_id, "www", path=path)

        # Actor properties
        @self.fastapi_app.get("/{actor_id}/properties")
        @self.fastapi_app.post("/{actor_id}/properties")
        @self.fastapi_app.put("/{actor_id}/properties")
        @self.fastapi_app.delete("/{actor_id}/properties")
        async def app_properties_root(actor_id: str, request: Request) -> Response:  # pyright: ignore[reportUnusedFunction]
            # Check authentication and redirect to Google OAuth2 if needed
            auth_redirect = await self._check_auth_or_redirect(request)
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(
                request, actor_id, "properties", name=""
            )

        # Property metadata endpoint (must come before catch-all {name:path})
        @self.fastapi_app.get("/{actor_id}/properties/{name}/metadata")
        @self.fastapi_app.put("/{actor_id}/properties/{name}/metadata")
        async def app_property_metadata(
            actor_id: str, request: Request, name: str
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            auth_redirect = await self._check_auth_or_redirect(request)
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(
                request, actor_id, "properties", name=name, metadata=True
            )

        # Property list items endpoint (must come before catch-all {name:path})
        @self.fastapi_app.get("/{actor_id}/properties/{name}/items")
        @self.fastapi_app.post("/{actor_id}/properties/{name}/items")
        async def app_property_items(
            actor_id: str, request: Request, name: str
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            auth_redirect = await self._check_auth_or_redirect(request)
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(
                request, actor_id, "properties", name=name, items=True
            )

        @self.fastapi_app.get("/{actor_id}/properties/{name:path}")
        @self.fastapi_app.post("/{actor_id}/properties/{name:path}")
        @self.fastapi_app.put("/{actor_id}/properties/{name:path}")
        @self.fastapi_app.delete("/{actor_id}/properties/{name:path}")
        async def app_properties(
            actor_id: str, request: Request, name: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            # Check authentication and redirect to Google OAuth2 if needed
            auth_redirect = await self._check_auth_or_redirect(request)
            if auth_redirect:
                return auth_redirect
            return await self._handle_actor_request(
                request, actor_id, "properties", name=name
            )

        # Actor trust - path-based endpoints (more specific routes first)
        # Shared properties endpoint (most specific, must be before general trust peer routes)
        @self.fastapi_app.get(
            "/{actor_id}/trust/{relationship}/{peerid}/shared_properties",
            summary="Get shared properties available for subscription",
            description=(
                "Returns properties that the authenticated peer has permission to subscribe to. "
                "Requires an active trust relationship. Response includes property names, "
                "display names, item counts (for property lists), and permitted operations. "
                "Authentication: Bearer token (OAuth2) or ActingWeb trust secret."
            ),
            tags=["Trust"],
            responses={
                200: {
                    "description": "List of shared and excluded properties",
                    "content": {
                        "application/json": {
                            "example": {
                                "actor_id": "actor-123",
                                "peer_id": "peer-456",
                                "relationship": "mcp_client",
                                "shared_properties": [
                                    {
                                        "name": "memory_personal",
                                        "display_name": "Memory Personal",
                                        "item_count": 42,
                                        "operations": ["read", "subscribe"],
                                    }
                                ],
                                "excluded_properties": ["memory_private"],
                            }
                        }
                    },
                },
                403: {"description": "Permission denied or wrong peer"},
                404: {"description": "Trust relationship not found"},
                503: {"description": "Permission system not available"},
            },
        )
        async def app_trust_shared_properties(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            relationship: str,
            peerid: str,
            request: Request,
        ) -> Response:
            return await self._handle_actor_request(
                request,
                actor_id,
                "trust",
                relationship=relationship,
                peerid=peerid,
                shared_properties=True,
            )

        @self.fastapi_app.get("/{actor_id}/trust/{relationship}/{peerid}")
        @self.fastapi_app.post("/{actor_id}/trust/{relationship}/{peerid}")
        @self.fastapi_app.put("/{actor_id}/trust/{relationship}/{peerid}")
        @self.fastapi_app.delete("/{actor_id}/trust/{relationship}/{peerid}")
        async def app_trust_peer(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            request: Request,
            relationship: str,
            peerid: str,
        ) -> Response:
            return await self._handle_actor_request(
                request, actor_id, "trust", relationship=relationship, peerid=peerid
            )

        @self.fastapi_app.get("/{actor_id}/trust/{relationship}")
        @self.fastapi_app.post("/{actor_id}/trust/{relationship}")
        @self.fastapi_app.put("/{actor_id}/trust/{relationship}")
        @self.fastapi_app.delete("/{actor_id}/trust/{relationship}")
        async def app_trust_relationship(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            request: Request,
            relationship: str,
        ) -> Response:
            return await self._handle_actor_request(
                request, actor_id, "trust", relationship=relationship, peerid=None
            )

        # Actor trust - root endpoint (least specific, defined last)
        @self.fastapi_app.get("/{actor_id}/trust")
        @self.fastapi_app.post("/{actor_id}/trust")
        @self.fastapi_app.put("/{actor_id}/trust")
        @self.fastapi_app.delete("/{actor_id}/trust")
        async def app_trust_root(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            request: Request,
        ) -> Response:
            return await self._handle_actor_request(
                request, actor_id, "trust", relationship=None, peerid=None
            )

        # Trust permission management endpoints
        @self.fastapi_app.get("/{actor_id}/trust/{relationship}/{peerid}/permissions")
        @self.fastapi_app.put("/{actor_id}/trust/{relationship}/{peerid}/permissions")
        @self.fastapi_app.delete(
            "/{actor_id}/trust/{relationship}/{peerid}/permissions"
        )
        async def app_trust_permissions(
            actor_id: str, request: Request, relationship: str, peerid: str
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request,
                actor_id,
                "trust",
                relationship=relationship,
                peerid=peerid,
                permissions=True,
            )

        # Actor subscriptions
        @self.fastapi_app.get("/{actor_id}/subscriptions")
        @self.fastapi_app.post("/{actor_id}/subscriptions")
        @self.fastapi_app.put("/{actor_id}/subscriptions")
        @self.fastapi_app.delete("/{actor_id}/subscriptions")
        @self.fastapi_app.get("/{actor_id}/subscriptions/{peerid}")
        @self.fastapi_app.post("/{actor_id}/subscriptions/{peerid}")
        @self.fastapi_app.put("/{actor_id}/subscriptions/{peerid}")
        @self.fastapi_app.delete("/{actor_id}/subscriptions/{peerid}")
        @self.fastapi_app.get("/{actor_id}/subscriptions/{peerid}/{subid}")
        @self.fastapi_app.post("/{actor_id}/subscriptions/{peerid}/{subid}")
        @self.fastapi_app.put("/{actor_id}/subscriptions/{peerid}/{subid}")
        @self.fastapi_app.delete("/{actor_id}/subscriptions/{peerid}/{subid}")
        @self.fastapi_app.get("/{actor_id}/subscriptions/{peerid}/{subid}/{seqnr:int}")
        async def app_subscriptions(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            request: Request,
            peerid: str | None = None,
            subid: str | None = None,
            seqnr: int | None = None,
        ) -> Response:
            return await self._handle_actor_request(
                request,
                actor_id,
                "subscriptions",
                peerid=peerid,
                subid=subid,
                seqnr=seqnr,
            )

        # Actor resources
        @self.fastapi_app.get("/{actor_id}/resources")
        @self.fastapi_app.post("/{actor_id}/resources")
        @self.fastapi_app.put("/{actor_id}/resources")
        @self.fastapi_app.delete("/{actor_id}/resources")
        @self.fastapi_app.get("/{actor_id}/resources/{name:path}")
        @self.fastapi_app.post("/{actor_id}/resources/{name:path}")
        @self.fastapi_app.put("/{actor_id}/resources/{name:path}")
        @self.fastapi_app.delete("/{actor_id}/resources/{name:path}")
        async def app_resources(
            actor_id: str, request: Request, name: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "resources", name=name
            )

        # Actor callbacks
        @self.fastapi_app.get("/{actor_id}/callbacks")
        @self.fastapi_app.post("/{actor_id}/callbacks")
        @self.fastapi_app.put("/{actor_id}/callbacks")
        @self.fastapi_app.delete("/{actor_id}/callbacks")
        @self.fastapi_app.get("/{actor_id}/callbacks/{name:path}")
        @self.fastapi_app.post("/{actor_id}/callbacks/{name:path}")
        @self.fastapi_app.put("/{actor_id}/callbacks/{name:path}")
        @self.fastapi_app.delete("/{actor_id}/callbacks/{name:path}")
        async def app_callbacks(
            actor_id: str, request: Request, name: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "callbacks", name=name
            )

        # Actor devtest
        @self.fastapi_app.get("/{actor_id}/devtest")
        @self.fastapi_app.post("/{actor_id}/devtest")
        @self.fastapi_app.put("/{actor_id}/devtest")
        @self.fastapi_app.delete("/{actor_id}/devtest")
        @self.fastapi_app.get("/{actor_id}/devtest/{path:path}")
        @self.fastapi_app.post("/{actor_id}/devtest/{path:path}")
        @self.fastapi_app.put("/{actor_id}/devtest/{path:path}")
        @self.fastapi_app.delete("/{actor_id}/devtest/{path:path}")
        async def app_devtest(
            actor_id: str, request: Request, path: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "devtest", path=path
            )

        # Actor methods
        @self.fastapi_app.get("/{actor_id}/methods")
        @self.fastapi_app.post("/{actor_id}/methods")
        @self.fastapi_app.put("/{actor_id}/methods")
        @self.fastapi_app.delete("/{actor_id}/methods")
        @self.fastapi_app.get("/{actor_id}/methods/{name:path}")
        @self.fastapi_app.post("/{actor_id}/methods/{name:path}")
        @self.fastapi_app.put("/{actor_id}/methods/{name:path}")
        @self.fastapi_app.delete("/{actor_id}/methods/{name:path}")
        async def app_methods(
            actor_id: str, request: Request, name: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "methods", name=name
            )

        # Actor actions
        @self.fastapi_app.get("/{actor_id}/actions")
        @self.fastapi_app.post("/{actor_id}/actions")
        @self.fastapi_app.put("/{actor_id}/actions")
        @self.fastapi_app.delete("/{actor_id}/actions")
        @self.fastapi_app.get("/{actor_id}/actions/{name:path}")
        @self.fastapi_app.post("/{actor_id}/actions/{name:path}")
        @self.fastapi_app.put("/{actor_id}/actions/{name:path}")
        @self.fastapi_app.delete("/{actor_id}/actions/{name:path}")
        async def app_actions(
            actor_id: str, request: Request, name: str = ""
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "actions", name=name
            )

        # Third-party service OAuth2 callbacks and management
        @self.fastapi_app.get("/{actor_id}/services/{service_name}/callback")
        async def app_services_callback(  # pyright: ignore[reportUnusedFunction]
            actor_id: str,
            service_name: str,
            request: Request,
            code: str | None = None,
            state: str | None = None,
            error: str | None = None,
        ) -> Response:
            return await self._handle_actor_request(
                request,
                actor_id,
                "services",
                name=service_name,
                code=code,
                state=state,
                error=error,
            )

        @self.fastapi_app.delete("/{actor_id}/services/{service_name}")
        async def app_services_revoke(
            actor_id: str, service_name: str, request: Request
        ) -> Response:  # pyright: ignore[reportUnusedFunction]
            return await self._handle_actor_request(
                request, actor_id, "services", name=service_name
            )

    async def _normalize_request(self, request: Request) -> dict[str, Any]:
        """Convert FastAPI request to ActingWeb format."""
        # Read body asynchronously
        body = await request.body()

        # Parse cookies
        cookies = {}
        raw_cookies = request.headers.get("cookie")
        if raw_cookies:
            for cookie in raw_cookies.split("; "):
                if "=" in cookie:
                    name, value = cookie.split("=", 1)
                    cookies[name] = value

        # Convert headers (preserve case-sensitive header names)
        headers = {}
        for k, v in request.headers.items():
            # FastAPI normalizes header names to lowercase, but we need to preserve case
            # for compatibility with ActingWeb's auth system
            if k.lower() == "authorization":
                headers["Authorization"] = v
                # Authorization header found (no logging needed for routine operation)
            elif k.lower() == "content-type":
                headers["Content-Type"] = v
            else:
                headers[k] = v

        # If no Authorization header but we have an oauth_token cookie (web UI session),
        # provide it as a Bearer token so core auth can validate OAuth2 and authorize creator actions.
        if "Authorization" not in headers and "oauth_token" in cookies:
            headers["Authorization"] = f"Bearer {cookies['oauth_token']}"
            self.logger.debug(
                "FastAPI: Injected Authorization Bearer from oauth_token cookie for web UI request"
            )

        # Get query parameters and form data (similar to Flask's request.values)
        params = {}
        # Start with query parameters
        for k, v in request.query_params.items():
            params[k] = v

        # Parse form data if content type is form-encoded
        content_type = headers.get("Content-Type", "")
        if "application/x-www-form-urlencoded" in content_type and body:
            try:
                from urllib.parse import parse_qs

                body_str = (
                    body.decode("utf-8") if isinstance(body, bytes) else str(body)
                )
                form_data = parse_qs(body_str, keep_blank_values=True)
                # parse_qs returns lists, but we want single values like Flask
                for k, v_list in form_data.items():
                    if v_list:
                        params[k] = v_list[0]  # Take first value, like Flask
            except (UnicodeDecodeError, ValueError) as e:
                self.logger.warning(f"Failed to parse form data: {e}")

        # Debug logging for trust endpoint
        if "/trust" in str(request.url.path) and params:
            self.logger.debug(f"Trust query params: {params}")

        return {
            "method": request.method,
            "path": str(request.url.path),
            "data": body,
            "headers": headers,
            "cookies": cookies,
            "values": params,
            "url": str(request.url),
        }

    def _create_fastapi_response(self, webobj: AWWebObj, request: Request) -> Response:
        """Convert ActingWeb response to FastAPI response."""
        if webobj.response.redirect:
            logger.debug(
                f"_create_fastapi_response: Creating redirect response to {webobj.response.redirect}"
            )
            response: Response = RedirectResponse(
                url=webobj.response.redirect, status_code=302
            )
        else:
            # Create appropriate response based on content type
            content_type = webobj.response.headers.get("Content-Type", "")
            if "application/json" in content_type:
                try:
                    json_content = (
                        json.loads(webobj.response.body) if webobj.response.body else {}
                    )
                    response = JSONResponse(
                        content=json_content, status_code=webobj.response.status_code
                    )
                except (json.JSONDecodeError, TypeError):
                    response = Response(
                        content=webobj.response.body,
                        status_code=webobj.response.status_code,
                        headers=webobj.response.headers,
                    )
            elif "text/html" in content_type:
                response = HTMLResponse(
                    content=webobj.response.body,
                    status_code=webobj.response.status_code,
                )
            else:
                response = Response(
                    content=webobj.response.body,
                    status_code=webobj.response.status_code,
                    headers=webobj.response.headers,
                )

        # Set additional headers
        for key, value in webobj.response.headers.items():
            if key.lower() not in ["content-type", "content-length"]:
                response.headers[key] = value

        # Set cookies
        for cookie in webobj.response.cookies:
            response.set_cookie(
                key=cookie["name"],
                value=cookie["value"],
                max_age=cookie.get("max_age"),
                secure=cookie.get("secure", False),
                httponly=cookie.get("httponly", False),
            )

        return response

    async def _handle_factory_request(self, request: Request) -> Response:
        """Handle factory requests (actor creation)."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        # Check if user is already authenticated and redirect to their actor
        oauth_cookie = request.cookies.get("oauth_token")
        self.logger.debug(
            f"Factory request: method={request.method}, has_oauth_cookie={bool(oauth_cookie)}"
        )
        if oauth_cookie and request.method == "GET":
            self.logger.debug(
                f"Processing GET request with OAuth cookie (length {len(oauth_cookie)})"
            )

            # First, check if this is an ActingWeb session token
            try:
                from ...oauth_session import get_oauth2_session_manager

                session_manager = get_oauth2_session_manager(self.aw_app.get_config())
                token_data = session_manager.validate_access_token(oauth_cookie)
                if token_data:
                    actor_id = token_data.get("actor_id")
                    if actor_id:
                        # Valid ActingWeb token - redirect to actor's www page
                        redirect_url = f"/{actor_id}/www"
                        self.logger.debug(
                            f"ActingWeb token valid - redirecting to {redirect_url}"
                        )
                        return RedirectResponse(url=redirect_url, status_code=302)
            except Exception as e:
                self.logger.debug(f"ActingWeb token validation failed: {e}")

            # Fall back to Google/GitHub OAuth token validation (legacy support)
            try:
                from ...oauth2 import create_oauth2_authenticator

                authenticator = create_oauth2_authenticator(self.aw_app.get_config())
                if authenticator.is_enabled():
                    self.logger.debug("OAuth2 is enabled, validating token...")
                    # Validate the token and get user info
                    user_info = authenticator.validate_token_and_get_user_info(
                        oauth_cookie
                    )
                    if user_info:
                        email = authenticator.get_email_from_user_info(
                            user_info, oauth_cookie
                        )
                        if email:
                            self.logger.debug(
                                f"Token validation successful for {email}"
                            )
                            # Look up actor by email
                            actor_instance = (
                                authenticator.lookup_or_create_actor_by_email(email)
                            )
                            if actor_instance and actor_instance.id:
                                # Redirect to actor's www page
                                redirect_url = f"/{actor_instance.id}/www"
                                self.logger.debug(
                                    f"Redirecting authenticated user {email} to {redirect_url}"
                                )
                                return RedirectResponse(
                                    url=redirect_url, status_code=302
                                )
                    # Token is invalid/expired - clear the cookie and redirect to new OAuth flow
                    self.logger.debug(
                        "OAuth token expired or invalid - clearing cookie and redirecting to OAuth"
                    )
                    original_url = str(request.url)
                    oauth_redirect = create_oauth_redirect_response(
                        self.aw_app.get_config(), redirect_after_auth=original_url
                    )
                    # Clear the expired cookie
                    oauth_redirect.delete_cookie("oauth_token", path="/")
                    return oauth_redirect
                else:
                    self.logger.warning("OAuth2 not enabled in config")
            except Exception as e:
                self.logger.error(f"OAuth token validation failed in factory: {e}")
                # Token validation failed - clear cookie and redirect to fresh OAuth
                self.logger.debug(
                    "OAuth token validation error - clearing cookie and redirecting to OAuth"
                )
                original_url = str(request.url)
                oauth_redirect = create_oauth_redirect_response(
                    self.aw_app.get_config(), redirect_after_auth=original_url
                )
                # Clear the invalid cookie
                oauth_redirect.delete_cookie("oauth_token", path="/")
                return oauth_redirect

        # Always use the standard factory handler
        handler = factory.RootFactoryHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        method_name = request.method.lower()
        handler_method = getattr(handler, method_name, None)
        if handler_method and callable(handler_method):
            # Run the synchronous handler in a thread pool to avoid blocking the event loop
            try:
                loop = asyncio.get_running_loop()
                await loop.run_in_executor(self.executor, handler_method)
            except (KeyboardInterrupt, SystemExit):
                # Don't catch system signals
                raise
            except Exception as e:
                # Log the error but let ActingWeb handlers set their own response codes
                self.logger.error(f"Error in factory handler: {e}")

                # Check if the handler already set an appropriate response code
                if webobj.response.status_code != 200:
                    # Handler already set a status code, respect it
                    self.logger.debug(
                        f"Handler set status code: {webobj.response.status_code}"
                    )
                else:
                    # For network/SSL errors, set appropriate status codes
                    error_message = str(e).lower()
                    if "ssl" in error_message or "certificate" in error_message:
                        webobj.response.set_status(
                            502, "Bad Gateway - SSL connection failed"
                        )
                    elif "connection" in error_message or "timeout" in error_message:
                        webobj.response.set_status(
                            503, "Service Unavailable - Connection failed"
                        )
                    else:
                        webobj.response.set_status(500, "Internal server error")
        else:
            raise HTTPException(status_code=405, detail="Method not allowed")

        # Handle template rendering for factory
        if request.method == "GET" and webobj.response.status_code == 200:
            if self.templates:
                return self.templates.TemplateResponse(
                    "aw-root-factory.html",
                    {"request": request, **webobj.response.template_values},
                )
        elif request.method == "POST":
            # Only render templates for form submissions, not JSON requests
            content_type = request.headers.get("content-type", "")
            is_json_request = "application/json" in content_type
            if (
                not is_json_request
                and webobj.response.status_code in [200, 201]
                and self.templates
            ):
                return self.templates.TemplateResponse(
                    "aw-root-created.html",
                    {"request": request, **webobj.response.template_values},
                )
            elif (
                not is_json_request
                and webobj.response.status_code == 400
                and self.templates
            ):
                return self.templates.TemplateResponse(
                    "aw-root-failed.html",
                    {"request": request, **webobj.response.template_values},
                )

        return self._create_fastapi_response(webobj, request)

    async def _handle_factory_get_request(self, request: Request) -> Response:
        """Handle GET requests to factory route - call factory handler to populate OAuth vars."""
        # Create webobj using the same pattern as _handle_factory_request
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        # Call the factory handler's get() method to populate OAuth template variables
        handler = factory.RootFactoryHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run handler in thread pool to avoid blocking
        loop = asyncio.get_event_loop()
        await loop.run_in_executor(self.executor, handler.get)

        # Render template with populated values (including oauth_enabled, oauth_providers, etc.)
        if self.templates and webobj.response.template_values:
            return self.templates.TemplateResponse(
                "aw-root-factory.html",
                {"request": request, **webobj.response.template_values},
            )
        elif self.templates:
            return self.templates.TemplateResponse(
                "aw-root-factory.html", {"request": request}
            )
        else:
            # Fallback for when templates are not available
            return Response(
                """
                <html>
                <head><title>ActingWeb Demo</title></head>
                <body>
                    <h1>Welcome to ActingWeb Demo</h1>
                    <form action="/" method="post">
                        <label>Your Email: <input type="email" name="creator" required /></label>
                        <input type="submit" value="Create Actor" />
                    </form>
                </body>
                </html>
            """,
                media_type="text/html",
            )

    async def _handle_factory_post_with_oauth_redirect(
        self, request: Request
    ) -> Response:
        """Handle POST to factory route with OAuth2 redirect including email hint."""
        try:
            # Parse the form data to extract email
            req_data = await self._normalize_request(request)
            email = None

            # Try to get email from JSON body first
            if req_data["data"]:
                try:
                    data = json.loads(req_data["data"])
                    email = data.get("creator") or data.get("email")
                except (json.JSONDecodeError, ValueError):
                    pass

            # Fallback to form data
            if not email:
                email = req_data["values"].get("creator") or req_data["values"].get(
                    "email"
                )

            if not email:
                # No email provided - return error or redirect back to form
                if self.templates:
                    return self.templates.TemplateResponse(
                        "aw-root-factory.html",
                        {"request": request, "error": "Email is required"},
                    )
                else:
                    raise HTTPException(status_code=400, detail="Email is required")

            self.logger.debug(f"Factory POST with email: {email}")

            # Create OAuth2 redirect with email hint
            try:
                from ...oauth2 import create_oauth2_authenticator

                authenticator = create_oauth2_authenticator(self.aw_app.get_config())
                if authenticator.is_enabled():
                    # Create authorization URL with email hint and User-Agent
                    redirect_after_auth = str(
                        request.url
                    )  # Redirect back to factory after auth
                    user_agent = request.headers.get("user-agent", "")
                    auth_url = authenticator.create_authorization_url(
                        redirect_after_auth=redirect_after_auth,
                        email_hint=email,
                        user_agent=user_agent,
                    )

                    self.logger.debug(f"Redirecting to OAuth2 with email hint: {email}")
                    return RedirectResponse(url=auth_url, status_code=302)
                else:
                    self.logger.warning(
                        "OAuth2 not configured - falling back to standard actor creation"
                    )
                    # Fall back to standard actor creation without OAuth
                    return await self._handle_factory_post_without_oauth(request, email)

            except Exception as e:
                self.logger.error(f"Error creating OAuth2 redirect: {e}")
                # Fall back to standard actor creation if OAuth2 setup fails
                self.logger.debug(
                    "OAuth2 setup failed - falling back to standard actor creation"
                )
                return await self._handle_factory_post_without_oauth(request, email)

        except HTTPException:
            raise
        except Exception as e:
            self.logger.error(f"Error in factory POST handler: {e}")
            raise HTTPException(status_code=500, detail="Internal server error") from e

    async def _handle_factory_post_without_oauth(
        self, request: Request, email: str
    ) -> Response:
        """Handle POST to factory route without OAuth2 - standard actor creation."""
        try:
            # Always use the standard factory handler
            req_data = await self._normalize_request(request)
            webobj = AWWebObj(
                url=req_data["url"],
                params=req_data["values"],
                body=req_data["data"],
                headers=req_data["headers"],
                cookies=req_data["cookies"],
            )

            # Use the standard factory handler
            handler = factory.RootFactoryHandler(
                webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
            )

            # Run the synchronous handler in a thread pool
            loop = asyncio.get_running_loop()
            await loop.run_in_executor(self.executor, handler.post)

            # Handle template rendering for factory
            if webobj.response.status_code in [200, 201]:
                if self.templates:
                    return self.templates.TemplateResponse(
                        "aw-root-created.html",
                        {"request": request, **webobj.response.template_values},
                    )
            elif webobj.response.status_code == 400:
                if self.templates:
                    return self.templates.TemplateResponse(
                        "aw-root-failed.html",
                        {"request": request, **webobj.response.template_values},
                    )

            return self._create_fastapi_response(webobj, request)

        except Exception as e:
            self.logger.error(f"Error in standard actor creation: {e}")
            if self.templates:
                return self.templates.TemplateResponse(
                    "aw-root-failed.html",
                    {"request": request, "error": "Actor creation failed"},
                )
            else:
                raise HTTPException(
                    status_code=500, detail="Actor creation failed"
                ) from e

    async def _handle_google_oauth_callback(self, request: Request) -> Response:
        """Handle Google OAuth2 callback."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.oauth2_callback import OAuth2CallbackHandler

        handler = OAuth2CallbackHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(self.executor, handler.get)

        # Handle redirect if needed
        if isinstance(result, dict) and result.get("redirect_required"):
            redirect_url = result.get("redirect_url")
            if redirect_url:
                webobj.response.set_redirect(redirect_url)
            else:
                # Convert result to JSON response
                webobj.response.body = json.dumps(result).encode("utf-8")
                webobj.response.headers["Content-Type"] = "application/json"

        # Handle OAuth2 errors with template rendering for better UX
        elif (
            isinstance(result, dict)
            and result.get("error")
            and webobj.response.status_code >= 400
        ):
            if self.templates and webobj.response.template_values:
                return self.templates.TemplateResponse(
                    "aw-root-failed.html",
                    {"request": request, **webobj.response.template_values},
                )

        return self._create_fastapi_response(webobj, request)

    async def _handle_oauth2_email(self, request: Request) -> Response:
        """Handle OAuth2 email input requests."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.oauth_email import OAuth2EmailHandler

        handler = OAuth2EmailHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        if request.method == "POST":
            await loop.run_in_executor(self.executor, handler.post)
        else:
            await loop.run_in_executor(self.executor, handler.get)

        # Handle template rendering for email form
        if (
            hasattr(webobj.response, "template_values")
            and webobj.response.template_values
        ):
            if self.templates:
                try:
                    # App provides aw-oauth-email.html template
                    return self.templates.TemplateResponse(
                        "aw-oauth-email.html",
                        {"request": request, **webobj.response.template_values},
                    )
                except Exception as e:
                    # Template not found - provide basic HTML form as fallback
                    self.logger.warning(f"Template aw-oauth-email.html not found: {e}")
                    session_id = webobj.response.template_values.get("session_id", "")
                    error = webobj.response.template_values.get("error", "")
                    provider = webobj.response.template_values.get(
                        "provider_display", "OAuth provider"
                    )
                    message = webobj.response.template_values.get(
                        "message",
                        f"Your {provider} account does not have a public email. Please enter your email address to continue.",
                    )

                    fallback_html = f"""
                    <!DOCTYPE html>
                    <html>
                    <head>
                        <title>Enter Email - ActingWeb</title>
                        <style>
                            body {{ font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }}
                            .error {{ color: red; margin-bottom: 15px; }}
                            .message {{ margin-bottom: 20px; color: #666; }}
                            input[type="email"] {{ width: 100%; padding: 10px; margin: 10px 0; }}
                            button {{ padding: 10px 20px; background: #4285f4; color: white; border: none; cursor: pointer; }}
                            button:hover {{ background: #357ae8; }}
                        </style>
                    </head>
                    <body>
                        <h1>Email Required</h1>
                        <p class="message">{message}</p>
                        {f'<p class="error">{error}</p>' if error else ""}
                        <form action="/oauth/email" method="POST">
                            <input type="hidden" name="session" value="{session_id}" />
                            <label>Email Address:
                                <input type="email" name="email" required placeholder="your@email.com" />
                            </label>
                            <button type="submit">Continue</button>
                        </form>
                    </body>
                    </html>
                    """
                    return HTMLResponse(content=fallback_html)

        return self._create_fastapi_response(webobj, request)

    async def _handle_email_verification(
        self, request: Request, actor_id: str
    ) -> Response:
        """Handle email verification requests."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.email_verification import EmailVerificationHandler

        handler = EmailVerificationHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        if request.method == "POST":
            await loop.run_in_executor(self.executor, handler.post)
        else:
            await loop.run_in_executor(self.executor, handler.get)

        # Handle template rendering for email verification
        if (
            hasattr(webobj.response, "template_values")
            and webobj.response.template_values
        ):
            if self.templates:
                try:
                    # App provides aw-verify-email.html template
                    return self.templates.TemplateResponse(
                        "aw-verify-email.html",
                        {"request": request, **webobj.response.template_values},
                    )
                except Exception as e:
                    # Template not found - provide basic HTML as fallback
                    self.logger.warning(f"Template aw-verify-email.html not found: {e}")
                    status = webobj.response.template_values.get("status", "")
                    webobj.response.template_values.get("message", "")
                    email = webobj.response.template_values.get("email", "")
                    error = webobj.response.template_values.get("error", "")

                    if status == "success":
                        fallback_html = f"""
                        <!DOCTYPE html>
                        <html>
                        <head><title>Email Verified</title></head>
                        <body>
                            <h1>✓ Email Verified!</h1>
                            <p>Your email address <strong>{email}</strong> has been verified.</p>
                            <p><a href="/{actor_id}/www">Continue to Dashboard</a></p>
                        </body>
                        </html>
                        """
                    elif status == "error":
                        fallback_html = f"""
                        <!DOCTYPE html>
                        <html>
                        <head><title>Verification Failed</title></head>
                        <body>
                            <h1>Verification Failed</h1>
                            <p>{error}</p>
                            <p><a href="/{actor_id}/www">Return to Dashboard</a></p>
                        </body>
                        </html>
                        """
                    else:
                        fallback_html = """
                        <!DOCTYPE html>
                        <html>
                        <head><title>Email Verification</title></head>
                        <body>
                            <h1>Email Verification</h1>
                            <p>Please check your email for a verification link.</p>
                        </body>
                        </html>
                        """
                    return HTMLResponse(content=fallback_html)

        return self._create_fastapi_response(webobj, request)

    async def _handle_oauth2_endpoint(
        self, request: Request, endpoint: str
    ) -> Response:
        """Handle OAuth2 endpoints (register, authorize, token)."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.oauth2_endpoints import OAuth2EndpointsHandler

        handler = OAuth2EndpointsHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        if request.method == "POST":
            result = await loop.run_in_executor(self.executor, handler.post, endpoint)
        elif request.method == "OPTIONS":
            result = await loop.run_in_executor(
                self.executor, handler.options, endpoint
            )
        else:
            result = await loop.run_in_executor(self.executor, handler.get, endpoint)

        # Check if handler set template values (for HTML response)
        if (
            hasattr(webobj.response, "template_values")
            and webobj.response.template_values
        ):
            self.logger.debug(
                f"OAuth2 template values found: {webobj.response.template_values}"
            )
            if self.templates:
                # This is an HTML template response
                template_name = (
                    "aw-oauth-authorization-form.html"  # Default OAuth2 template
                )
                try:
                    self.logger.debug(f"Attempting to render template: {template_name}")
                    return self.templates.TemplateResponse(
                        template_name,
                        {"request": request, **webobj.response.template_values},
                    )
                except Exception as e:
                    # Template not found or rendering error - fall back to JSON
                    self.logger.error(f"Template rendering failed: {e}")
                    from fastapi.responses import JSONResponse

                    return JSONResponse(
                        content={
                            "error": "template_error",
                            "error_description": f"Failed to render template: {str(e)}",
                            "template_values": webobj.response.template_values,
                        }
                    )
            else:
                self.logger.warning(
                    "Template values found but templates not initialized"
                )

        # Handle redirect responses (e.g., OAuth2 callbacks)
        if isinstance(result, dict) and result.get("status") == "redirect":
            redirect_url = result.get("location")
            if redirect_url:
                from fastapi.responses import RedirectResponse

                # Add CORS headers for OAuth2 redirect responses
                cors_headers = {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                    "Access-Control-Allow-Headers": "Authorization, Content-Type, mcp-protocol-version",
                }

                return RedirectResponse(
                    url=redirect_url, status_code=302, headers=cors_headers
                )

        # Return the OAuth2 result as JSON with CORS headers
        from fastapi.responses import JSONResponse

        # Add CORS headers for OAuth2 endpoints
        # Logout needs SPA CORS (echo origin + credentials) for cookie clearing to work
        # in cross-origin scenarios. Other endpoints use wildcard CORS.
        if endpoint == "logout":
            # Headers may be lowercase (FastAPI normalizes them)
            origin = req_data["headers"].get("origin", "") or req_data["headers"].get(
                "Origin", ""
            )
            cors_headers = {
                "Access-Control-Allow-Origin": origin if origin else "*",
                "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept",
                "Access-Control-Allow-Credentials": "true",
                "Access-Control-Max-Age": "86400",
            }
        else:
            cors_headers = {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                "Access-Control-Allow-Headers": "Authorization, Content-Type, mcp-protocol-version",
                "Access-Control-Max-Age": "86400",
            }

        # Merge handler headers (e.g., WWW-Authenticate) with CORS headers
        response_headers = dict(cors_headers)
        if hasattr(webobj.response, "headers"):
            for key, value in webobj.response.headers.items():
                response_headers[key] = value

        # Use status code from handler if set (e.g., 201 for client registration)
        status_code = (
            webobj.response.status_code
            if hasattr(webobj.response, "status_code")
            else 200
        )
        response = JSONResponse(
            content=result, headers=response_headers, status_code=status_code
        )

        # Copy cookies from handler response (e.g., for logout)
        if hasattr(webobj.response, "cookies"):
            for cookie_data in webobj.response.cookies:
                # FastAPI uses 'key' instead of 'name' for cookie name
                response.set_cookie(
                    key=cookie_data["name"],
                    value=cookie_data["value"],
                    max_age=cookie_data.get("max_age"),
                    secure=cookie_data.get("secure", False),
                    httponly=cookie_data.get("httponly", False),
                    path=cookie_data.get("path", "/"),
                    samesite=cookie_data.get("samesite", "lax"),
                )

        return response

    async def _handle_oauth2_spa_endpoint(
        self, request: Request, endpoint: str
    ) -> Response:
        """Handle SPA OAuth2 endpoints (config, authorize, token, revoke, session, logout)."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.oauth2_spa import OAuth2SPAHandler

        handler = OAuth2SPAHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        if request.method == "POST":
            result = await loop.run_in_executor(self.executor, handler.post, endpoint)
        elif request.method == "OPTIONS":
            result = await loop.run_in_executor(
                self.executor, handler.options, endpoint
            )
        else:
            result = await loop.run_in_executor(self.executor, handler.get, endpoint)

        # SPA endpoints always return JSON
        from fastapi.responses import JSONResponse

        # Get origin for CORS
        origin = req_data["headers"].get("origin", "*")

        # Add CORS headers for SPA endpoints
        cors_headers = {
            "Access-Control-Allow-Origin": origin,
            "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
            "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept",
            "Access-Control-Allow-Credentials": "true",
            "Access-Control-Max-Age": "86400",
        }

        # Use status code from handler if set
        status_code = (
            webobj.response.status_code
            if hasattr(webobj.response, "status_code") and webobj.response.status_code
            else 200
        )

        response = JSONResponse(
            content=result, headers=cors_headers, status_code=status_code
        )

        # Copy cookies from handler response
        if hasattr(webobj.response, "cookies"):
            for cookie_data in webobj.response.cookies:
                # FastAPI uses 'key' instead of 'name' for cookie name
                response.set_cookie(
                    key=cookie_data["name"],
                    value=cookie_data["value"],
                    max_age=cookie_data.get("max_age"),
                    secure=cookie_data.get("secure", False),
                    httponly=cookie_data.get("httponly", False),
                    path=cookie_data.get("path", "/"),
                    samesite=cookie_data.get("samesite", "lax"),
                )

        return response

    async def _handle_spa_session_retrieve(
        self, request: Request, session_id: str
    ) -> Response:
        """
        Handle retrieval of pending SPA session data after OAuth callback.

        The OAuth callback handler processes the authorization code and stores
        the resulting tokens in a pending session. The SPA then retrieves this
        data using the session_id passed in the redirect URL.
        """
        from fastapi.responses import JSONResponse

        from ...oauth_session import get_oauth2_session_manager

        # Get origin for CORS
        req_data = await self._normalize_request(request)
        origin = req_data["headers"].get("origin", "*")

        cors_headers = {
            "Access-Control-Allow-Origin": origin,
            "Access-Control-Allow-Methods": "GET, OPTIONS",
            "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept",
            "Access-Control-Allow-Credentials": "true",
        }

        try:
            session_manager = get_oauth2_session_manager(self.aw_app.get_config())

            # Look up the pending session
            # The session was stored with pending_session_id as part of token_data
            session_data = session_manager.get_session(session_id)

            if not session_data:
                return JSONResponse(
                    content={"error": True, "message": "Session not found or expired"},
                    status_code=404,
                    headers=cors_headers,
                )

            # Extract the token data that was stored
            token_data = session_data.get("token_data", {})

            if not token_data or "access_token" not in token_data:
                return JSONResponse(
                    content={"error": True, "message": "Invalid session data"},
                    status_code=400,
                    headers=cors_headers,
                )

            # Pending session will expire naturally (short TTL)
            # This is one-time use by design - second retrieval will fail after expiry

            # Return the session data
            response_data = {
                "success": True,
                "access_token": token_data.get("access_token"),
                "actor_id": token_data.get("actor_id"),
                "email": token_data.get("email"),
                "expires_at": token_data.get("expires_at"),
                "redirect_url": token_data.get("redirect_url"),
            }

            return JSONResponse(
                content=response_data,
                status_code=200,
                headers=cors_headers,
            )

        except Exception as e:
            self.logger.error(f"Error retrieving SPA session: {e}")
            return JSONResponse(
                content={"error": True, "message": "Failed to retrieve session"},
                status_code=500,
                headers=cors_headers,
            )

    async def _handle_bot_request(self, request: Request) -> Response:
        """Handle bot requests."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        handler = bot.BotHandler(
            webobj=webobj, config=self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(self.executor, handler.post, "/bot")

        return self._create_fastapi_response(webobj, request)

    async def _handle_mcp_request(self, request: Request) -> Response:
        """Handle MCP requests."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        handler = mcp.MCPHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Execute appropriate method based on request method
        if request.method == "GET":
            # Run the synchronous handler in a thread pool
            loop = asyncio.get_running_loop()
            result = await loop.run_in_executor(self.executor, handler.get)
        elif request.method == "POST":
            # Parse JSON body for POST requests
            try:
                if webobj.request.body:
                    data = json.loads(webobj.request.body)
                else:
                    data = {}
            except (json.JSONDecodeError, ValueError):
                data = {}

            # Run the synchronous handler in a thread pool
            loop = asyncio.get_running_loop()
            result = await loop.run_in_executor(self.executor, handler.post, data)
        else:
            raise HTTPException(status_code=405, detail="Method not allowed")

        # Create JSON response - check if handler set a different status code (e.g., 401 for auth)
        status_code = 200
        headers = {}

        # Check if the handler set a custom status code in the response object
        if hasattr(webobj, "response") and hasattr(webobj.response, "status_code"):
            status_code = webobj.response.status_code

        # Check if the handler set custom headers (e.g., WWW-Authenticate for OAuth2)
        if hasattr(webobj, "response") and hasattr(webobj.response, "headers"):
            headers = dict(webobj.response.headers)

        return JSONResponse(content=result, status_code=status_code, headers=headers)

    async def _handle_oauth2_discovery_endpoint(
        self, request: Request, endpoint: str
    ) -> JSONResponse:
        """Handle OAuth2 discovery endpoints that return JSON directly."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        from ...handlers.oauth2_endpoints import OAuth2EndpointsHandler

        handler = OAuth2EndpointsHandler(
            webobj, self.aw_app.get_config(), hooks=self.aw_app.hooks
        )

        # Run the synchronous handler in a thread pool
        loop = asyncio.get_running_loop()

        if request.method == "OPTIONS":
            result = await loop.run_in_executor(
                self.executor, handler.options, endpoint
            )
        else:
            result = await loop.run_in_executor(self.executor, handler.get, endpoint)

        # Add CORS headers directly for OAuth2 discovery endpoints
        cors_headers = {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, OPTIONS",
            "Access-Control-Allow-Headers": "Authorization, Content-Type, mcp-protocol-version",
            "Access-Control-Max-Age": "86400",
        }

        return JSONResponse(content=result, headers=cors_headers)

    async def _handle_actor_request(
        self, request: Request, actor_id: str, endpoint: str, **kwargs: Any
    ) -> Response:
        """Handle actor-specific requests."""
        req_data = await self._normalize_request(request)
        webobj = AWWebObj(
            url=req_data["url"],
            params=req_data["values"],
            body=req_data["data"],
            headers=req_data["headers"],
            cookies=req_data["cookies"],
        )

        # Get appropriate handler
        handler = self._get_handler(endpoint, webobj, actor_id, **kwargs)
        if not handler:
            raise HTTPException(status_code=404, detail="Handler not found")

        # Execute handler method
        method_name = request.method.lower()
        handler_method = getattr(handler, method_name, None)
        if handler_method and callable(handler_method):
            # Build positional arguments based on endpoint and kwargs
            args = [actor_id]
            extra_kwargs = {}  # Initialize extra_kwargs for all endpoints

            if endpoint == "meta":
                args.append(kwargs.get("path", ""))
            elif endpoint == "trust":
                # Only pass path parameters if they exist, let handler read query params from request
                relationship = kwargs.get("relationship")
                peerid = kwargs.get("peerid")

                # Support UI forms that send GET /trust/<peerid>?_method=DELETE|PUT
                # by interpreting the single path segment as a peer ID.
                # We detect this when:
                #  - there is only one path param provided
                #  - a method override is present requesting DELETE or PUT
                #  - no explicit peerid path param is provided
                method_override = (webobj.request.get("_method") or "").upper()
                if relationship and not peerid and method_override in ("DELETE", "PUT"):
                    # Heuristic: treat the "relationship" path part as a peer ID.
                    # Pass empty relationship (type) and the detected peer ID.
                    args.append("")
                    args.append(relationship)
                    self.logger.debug(
                        f"Trust handler args adjusted for method override: {args} (peerid assumed from single segment)"
                    )
                else:
                    if relationship:
                        args.append(relationship)
                        if peerid:
                            args.append(peerid)
                    self.logger.debug(f"Trust handler args: {args}, kwargs: {kwargs}")
            elif endpoint == "subscriptions":
                if kwargs.get("peerid"):
                    args.append(kwargs["peerid"])
                if kwargs.get("subid"):
                    args.append(kwargs["subid"])
                if kwargs.get("seqnr"):
                    args.append(kwargs["seqnr"])
            elif endpoint in [
                "www",
                "properties",
                "callbacks",
                "resources",
                "devtest",
                "methods",
                "actions",
                "services",
            ]:
                # These endpoints take a path/name parameter
                param_name = "path" if endpoint in ["www", "devtest"] else "name"
                args.append(kwargs.get(param_name, ""))

                # Services need additional kwargs for OAuth callback parameters
                if endpoint == "services":
                    # Pass code, state, error as kwargs to the handler
                    extra_kwargs = {
                        k: v
                        for k, v in kwargs.items()
                        if k in ["code", "state", "error"] and v is not None
                    }

            # Check for async handler method variant (e.g., post_async)
            async_method_name = f"{method_name}_async"
            async_handler_method = getattr(handler, async_method_name, None)

            try:
                if (
                    async_handler_method
                    and callable(async_handler_method)
                    and inspect.iscoroutinefunction(async_handler_method)
                ):
                    # Use native async handler - no thread pool overhead
                    self.logger.debug(
                        f"Using async handler {async_method_name} for {endpoint}"
                    )
                    if extra_kwargs:
                        await async_handler_method(*args, **extra_kwargs)  # type: ignore
                    else:
                        await async_handler_method(*args)  # type: ignore
                else:
                    # Fall back to sync handler in thread pool
                    loop = asyncio.get_running_loop()
                    if extra_kwargs:
                        # For services endpoint, pass extra kwargs
                        await loop.run_in_executor(
                            self.executor, lambda: handler_method(*args, **extra_kwargs)
                        )
                    else:
                        await loop.run_in_executor(self.executor, handler_method, *args)
            except (KeyboardInterrupt, SystemExit):
                # Don't catch system signals
                raise
            except Exception as e:
                # Log the error but let ActingWeb handlers set their own response codes
                self.logger.error(f"Error in {endpoint} handler: {e}")

                # Check if the handler already set an appropriate response code
                if webobj.response.status_code != 200:
                    # Handler already set a status code, respect it
                    self.logger.debug(
                        f"Handler set status code: {webobj.response.status_code}"
                    )
                else:
                    # For network/SSL errors, set appropriate status codes
                    error_message = str(e).lower()
                    if "ssl" in error_message or "certificate" in error_message:
                        webobj.response.set_status(
                            502, "Bad Gateway - SSL connection failed"
                        )
                    elif "connection" in error_message or "timeout" in error_message:
                        webobj.response.set_status(
                            503, "Service Unavailable - Connection failed"
                        )
                    else:
                        webobj.response.set_status(500, "Internal server error")
        else:
            raise HTTPException(status_code=405, detail="Method not allowed")

        # Special handling for www endpoint templates
        if (
            endpoint == "www"
            and request.method == "GET"
            and webobj.response.status_code == 200
            and self.templates
        ):
            path = kwargs.get("path", "")
            template_map = {
                "": "aw-actor-www-root.html",
                "init": "aw-actor-www-init.html",
                "properties": "aw-actor-www-properties.html",
                "property": "aw-actor-www-property.html",
                "trust": "aw-actor-www-trust.html",
                "trust/new": "aw-actor-www-trust-new.html",
            }
            template_name = template_map.get(path)

            # Handle individual property pages like "properties/notes", "properties/demo_version"
            if not template_name and path.startswith("properties/"):
                # This is an individual property page
                template_name = "aw-actor-www-property.html"

            # Check for custom template name from callback hook
            if (
                not template_name
                and hasattr(webobj.response, "template_name")
                and webobj.response.template_name
            ):
                template_name = webobj.response.template_name

            if template_name:
                return self.templates.TemplateResponse(
                    template_name,
                    {"request": request, **webobj.response.template_values},
                )

        return self._create_fastapi_response(webobj, request)

    def _get_handler(
        self, endpoint: str, webobj: AWWebObj, actor_id: str, **kwargs: Any
    ) -> Any | None:
        """Get the appropriate handler for an endpoint."""
        config = self.aw_app.get_config()

        # Special handling for services endpoint (not in base class)
        if endpoint == "services":
            return self._create_services_handler(webobj, config)

        # Use base class handler selection for all other endpoints
        return self.get_handler_class(endpoint, webobj, config, **kwargs)

    def _create_oauth_discovery_response(self) -> dict[str, Any]:
        """Create OAuth2 Authorization Server Discovery response (RFC 8414)."""
        config = self.aw_app.get_config()
        base_url = f"{config.proto}{config.fqdn}"
        oauth_provider = getattr(config, "oauth2_provider", "google")

        if oauth_provider == "google":
            return {
                "issuer": base_url,
                "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
                "token_endpoint": "https://oauth2.googleapis.com/token",
                "userinfo_endpoint": "https://www.googleapis.com/oauth2/v2/userinfo",
                "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
                "scopes_supported": ["openid", "email", "profile"],
                "response_types_supported": ["code"],
                "grant_types_supported": ["authorization_code", "refresh_token"],
                "subject_types_supported": ["public"],
                "id_token_signing_alg_values_supported": ["RS256"],
                "code_challenge_methods_supported": ["S256"],
                "token_endpoint_auth_methods_supported": [
                    "client_secret_post",
                    "client_secret_basic",
                ],
            }
        elif oauth_provider == "github":
            return {
                "issuer": base_url,
                "authorization_endpoint": "https://github.com/login/oauth/authorize",
                "token_endpoint": "https://github.com/login/oauth/access_token",
                "userinfo_endpoint": "https://api.github.com/user",
                "scopes_supported": ["user:email"],
                "response_types_supported": ["code"],
                "grant_types_supported": ["authorization_code"],
                "subject_types_supported": ["public"],
                "token_endpoint_auth_methods_supported": [
                    "client_secret_post",
                    "client_secret_basic",
                ],
            }
        else:
            return {"error": "Unknown OAuth provider"}

    def _create_mcp_info_response(self) -> dict[str, Any]:
        """Create MCP information response."""
        config = self.aw_app.get_config()
        base_url = f"{config.proto}{config.fqdn}"
        oauth_provider = getattr(config, "oauth2_provider", "google")

        return {
            "mcp_enabled": True,
            "mcp_endpoint": "/mcp",
            "authentication": {
                "type": "oauth2",
                "provider": "actingweb",
                "required_scopes": ["mcp"],
                "flow": "authorization_code",
                "auth_url": f"{base_url}/oauth/authorize",
                "token_url": f"{base_url}/oauth/token",
                "callback_url": f"{base_url}/oauth/callback",
                "registration_endpoint": f"{base_url}/oauth/register",
                "authorization_endpoint": f"{base_url}/oauth/authorize",
                "token_endpoint": f"{base_url}/oauth/token",
                "discovery_url": f"{base_url}/.well-known/oauth-authorization-server",
                "resource_discovery_url": f"{base_url}/.well-known/oauth-protected-resource",
                "enabled": True,
            },
            "supported_features": ["tools", "prompts"],
            "tools_count": 4,  # search, fetch, create_note, create_reminder
            "prompts_count": 3,  # analyze_notes, create_learning_prompt, create_meeting_prep
            "actor_lookup": "email_based",
            "description": f"ActingWeb MCP Demo - AI can interact with actors through MCP protocol using {oauth_provider.title()} OAuth2",
        }

    def _create_services_handler(self, webobj: AWWebObj, config) -> Any:
        """Create services handler with service registry injection."""
        handler = services.ServicesHandler(webobj, config, hooks=self.aw_app.hooks)
        # Inject service registry into the handler so it can access it
        handler._service_registry = self.aw_app.get_service_registry()
        return handler
