"""FastAPI wrapper for hybrid MCP + REST API server.

This module provides a FastAPI application that serves both:
1. MCP protocol at /mcp (for AI clients like Claude, Cursor)
2. REST API at /api/* (for web apps, curl, Postman)
3. Swagger documentation at /docs (auto-generated)
4. Health check at /health (for monitoring)

Architecture (STORY-023f):
- Shares lifespan handler with MCP server (single resource set)
- Reuses existing services (TestService, ProductService, etc.)
- Delegates all business logic to service layer
- Exception handlers convert domain exceptions to HTTP status codes

Key Design Decisions:
- NESTED lifespan pattern: MCP lifespan is outer context
- No CORS middleware (FastMCP handles it internally)
- FastAPI response_model uses Pydantic models directly (no inline_schema_refs needed)
"""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from time import monotonic
from typing import Any, cast

from fastapi import FastAPI, HTTPException, Path, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

from testio_mcp.exceptions import (
    ProductNotFoundException,
    TestIOAPIError,
    TestNotFoundException,
)
from testio_mcp.schemas.tests import (
    ListTestsOutput,
    PaginationInfo,
    ProductInfoSummary,
    TestStatusOutput,
)
from testio_mcp.server import ServerContext, mcp
from testio_mcp.server import lifespan as mcp_lifespan
from testio_mcp.services.multi_test_report_service import MultiTestReportService
from testio_mcp.services.product_service import ProductService
from testio_mcp.services.test_service import TestService
from testio_mcp.tools.generate_ebr_report_tool import GenerateEBROutput
from testio_mcp.tools.list_products_tool import ListProductsOutput
from testio_mcp.transformers import to_test_summary_list
from testio_mcp.utilities import parse_status_input
from testio_mcp.utilities.service_helpers import get_service_from_server_context


# Pydantic models for REST API responses
class DatabaseInfo(BaseModel):
    """Database connection and statistics."""

    connected: bool = Field(description="Database connection status", examples=[True])
    total_tests: int | None = Field(
        default=None, description="Total tests in database", examples=[712]
    )
    total_products: int | None = Field(
        default=None, description="Total products in database", examples=[6]
    )
    database_size_mb: float | None = Field(
        default=None, description="Database size in MB", examples=[309.18]
    )


class HealthCheckOutput(BaseModel):
    """Health check response."""

    status: str = Field(description="Health status", examples=["healthy"])
    version: str = Field(description="Server version", examples=["1.0.0"])
    uptime_seconds: float | None = Field(
        default=None, description="Server uptime in seconds", examples=[1234.56]
    )
    database: DatabaseInfo = Field(description="Database connection and stats")
    error: str | None = Field(default=None, description="Error message if unhealthy")


# Create MCP HTTP app with path='/' since it will be mounted at /mcp
# The path parameter tells MCP what internal path to use
# When mounted at /mcp, requests to /mcp/ will be handled
mcp_app = mcp.http_app(path="/")


@asynccontextmanager
async def hybrid_lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Share MCP lifespan with FastAPI.

    CRITICAL: This ensures single TestIOClient/PersistentCache instance
    and one set of background tasks (no duplication).

    Pattern: NESTED async context manager
    - MCP server lifespan is OUTER context (initializes resources)
    - MCP app lifespan is INNER context (initializes session manager)
    - Ensures resources exist before FastAPI routes start
    - ServerContext stored in app.state for REST endpoint access

    Source: https://gofastmcp.com/integrations/fastapi.md
    """
    start_time = monotonic()

    # Use the same lifespan as MCP (single resource set)
    # CRITICAL: Must nest both mcp_lifespan AND mcp_app.lifespan
    async with mcp_lifespan(mcp) as server_ctx:
        # Initialize MCP app's session manager
        async with mcp_app.lifespan(app):
            # Expose ServerContext to FastAPI endpoints
            app.state.server_context = server_ctx
            app.state.start_time = start_time
            yield


# Create FastAPI wrapper
api = FastAPI(
    title="TestIO MCP Server",
    description="Hybrid MCP + REST API for TestIO Customer API",
    version="1.0.0",
    lifespan=hybrid_lifespan,  # ⚠️ CRITICAL: Share lifespan
    docs_url="/docs",
    redoc_url="/redoc",
    openapi_url="/openapi.json",
)

# Mount MCP protocol at /mcp (handles both /mcp and /mcp/)
# Note: FastAPI's mount() requires exact path match, so we mount at root of /mcp
# and the MCP app handles its own routing from there
api.mount("/mcp", mcp_app)


# Helper to get ServerContext from request
def get_server_context_from_request(request: Request) -> ServerContext:
    """Extract ServerContext from FastAPI app state.

    Args:
        request: FastAPI request object

    Returns:
        ServerContext with testio_client and cache

    Raises:
        RuntimeError: If server context not initialized
    """
    server_ctx = getattr(request.app.state, "server_context", None)
    if server_ctx is None:
        raise RuntimeError("Server context not initialized")
    return cast(ServerContext, server_ctx)


# Exception handlers


@api.exception_handler(TestNotFoundException)
async def handle_test_not_found(_: Request, exc: TestNotFoundException) -> JSONResponse:
    """Convert TestNotFoundException to HTTP 404."""
    return JSONResponse(
        status_code=404,
        content={
            "error": "test_not_found",
            "message": exc.message,
            "test_id": exc.test_id,
        },
    )


@api.exception_handler(ProductNotFoundException)
async def handle_product_not_found(_: Request, exc: ProductNotFoundException) -> JSONResponse:
    """Convert ProductNotFoundException to HTTP 404."""
    return JSONResponse(
        status_code=404,
        content={
            "error": "product_not_found",
            "message": exc.message,
            "product_id": exc.product_id,
        },
    )


@api.exception_handler(TestIOAPIError)
async def handle_api_error(_: Request, exc: TestIOAPIError) -> JSONResponse:
    """Convert TestIOAPIError to appropriate HTTP status code."""
    # Use original status code if it's a valid HTTP error (4xx/5xx)
    # Otherwise, treat as 502 Bad Gateway (upstream error)
    status = exc.status_code if 400 <= exc.status_code < 600 else 502
    return JSONResponse(
        status_code=status,
        content={
            "error": "upstream_api_error",
            "message": exc.message,
            "status_code": exc.status_code,
        },
    )


# REST Endpoints (Task 4)
# Test Endpoints


@api.get("/api/tests/{test_id}", response_model=TestStatusOutput)
async def get_test_rest(
    request: Request,
    test_id: int = Path(..., description="Test ID", gt=0),
) -> TestStatusOutput:
    """Get test status via REST API.

    Returns test details with bugs, same as MCP tool.
    """
    # Get ServerContext from FastAPI app state
    server_ctx = get_server_context_from_request(request)

    # Create service using DI helper (reuses MCP infrastructure)
    service = get_service_from_server_context(server_ctx, TestService)

    # Delegate to service (exception handlers convert to HTTP errors)
    result = await service.get_test_status(test_id)

    # Service returns dict that matches TestStatusOutput structure
    return TestStatusOutput(**result)


@api.get("/api/tests", response_model=ListTestsOutput)
async def list_tests_rest(
    request: Request,
    product_id: int = Query(..., description="Product ID", gt=0),
    statuses: str | None = Query(
        None,
        description="Comma-separated test statuses (e.g., 'running,locked')",
    ),
    page: int = Query(1, description="Page number (1-indexed)", ge=1),
    per_page: int = Query(100, description="Items per page", ge=1, le=200),
) -> ListTestsOutput:
    """List tests for a product with filtering.

    Query parameters:
    - product_id: Product ID (required)
    - statuses: Comma-separated status values (optional)
    - page: Page number, 1-indexed (default: 1)
    - per_page: Items per page (default: 100, max: 200)
    """
    # Get ServerContext from FastAPI app state
    server_ctx = get_server_context_from_request(request)

    # Create service
    service = get_service_from_server_context(server_ctx, TestService)

    # Parse statuses using centralized parser with error handling
    try:
        status_list = parse_status_input(statuses)
    except ValueError as e:
        raise HTTPException(
            status_code=422,
            detail=f"Invalid status parameter: {e}",
        ) from e

    # Delegate to service
    service_result = await service.list_tests(
        product_id=product_id,
        statuses=status_list,  # type: ignore[arg-type]
        page=page,
        per_page=per_page,
    )

    # Extract service result
    product = service_result["product"]
    tests = service_result["tests"]
    statuses_filter = service_result["statuses_filter"]
    total_count = service_result["total_count"]
    offset = service_result["offset"]
    has_more = service_result["has_more"]

    # Transform using centralized transformer
    test_summaries = to_test_summary_list(tests)

    # Build pagination info
    pagination = PaginationInfo(
        page=page,
        per_page=per_page,
        offset=offset,
        start_index=offset,
        end_index=offset + len(tests) - 1 if tests else -1,
        total_count=total_count,
        has_more=has_more,
    )

    # Build product info
    product_info = ProductInfoSummary(
        id=product["id"],
        name=product["name"],
        type=product["type"],
    )

    # Return validated Pydantic model
    return ListTestsOutput(
        product=product_info,
        statuses_filter=statuses_filter,
        pagination=pagination,
        total_tests=len(test_summaries),
        tests=test_summaries,
    )


# Product Endpoints


@api.get("/api/products", response_model=ListProductsOutput)
async def list_products_rest(
    request: Request,
    search: str | None = Query(None, description="Search term for product name/description"),
    product_type: str | None = Query(
        None,
        description="Comma-separated product types (e.g., 'website,mobile_app_ios')",
    ),
) -> Any:
    """List products with optional filtering.

    Query parameters:
    - search: Search term (optional)
    - product_type: Comma-separated product types (optional)
    """
    # Get ServerContext from FastAPI app state
    server_ctx = get_server_context_from_request(request)

    # Create service
    service = get_service_from_server_context(server_ctx, ProductService)

    # Parse product types (comma-separated string to list)
    type_list = None
    if product_type:
        type_list = [t.strip() for t in product_type.split(",") if t.strip()]

    # Delegate to service
    result = await service.list_products(
        search=search,
        product_type=type_list,
    )

    # Service returns dict that matches ListProductsOutput structure
    return ListProductsOutput(**result)


@api.get("/api/products/{product_id}/tests", response_model=ListTestsOutput)
async def list_product_tests_rest(
    request: Request,
    product_id: int = Path(..., description="Product ID", gt=0),
    statuses: str | None = Query(
        None,
        description="Comma-separated test statuses (e.g., 'running,locked')",
    ),
    page: int = Query(1, description="Page number (1-indexed)", ge=1),
    per_page: int = Query(100, description="Items per page", ge=1, le=200),
) -> ListTestsOutput:
    """List tests for a specific product.

    Path parameters:
    - product_id: Product ID

    Query parameters:
    - statuses: Comma-separated status values (optional)
    - page: Page number, 1-indexed (default: 1)
    - per_page: Items per page (default: 100, max: 200)
    """
    # Get ServerContext from FastAPI app state
    server_ctx = get_server_context_from_request(request)

    # Create service
    service = get_service_from_server_context(server_ctx, TestService)

    # Parse statuses using centralized parser with error handling
    try:
        status_list = parse_status_input(statuses)
    except ValueError as e:
        raise HTTPException(
            status_code=422,
            detail=f"Invalid status parameter: {e}",
        ) from e

    # Delegate to service
    service_result = await service.list_tests(
        product_id=product_id,
        statuses=status_list,  # type: ignore[arg-type]
        page=page,
        per_page=per_page,
    )

    # Extract service result
    product = service_result["product"]
    tests = service_result["tests"]
    statuses_filter = service_result["statuses_filter"]
    total_count = service_result["total_count"]
    offset = service_result["offset"]
    has_more = service_result["has_more"]

    # Transform using centralized transformer
    test_summaries = to_test_summary_list(tests)

    # Build pagination info
    pagination = PaginationInfo(
        page=page,
        per_page=per_page,
        offset=offset,
        start_index=offset,
        end_index=offset + len(tests) - 1 if tests else -1,
        total_count=total_count,
        has_more=has_more,
    )

    # Build product info
    product_info = ProductInfoSummary(
        id=product["id"],
        name=product["name"],
        type=product["type"],
    )

    # Return validated Pydantic model
    return ListTestsOutput(
        product=product_info,
        statuses_filter=statuses_filter,
        pagination=pagination,
        total_tests=len(test_summaries),
        tests=test_summaries,
    )


# Report Endpoints


class EBRReportRequest(BaseModel):
    """Request body for EBR report generation."""

    product_id: int = Field(..., description="Product ID", gt=0)
    start_date: str | None = Field(
        None,
        description="Start date (ISO format or business terms like 'last 30 days')",
    )
    end_date: str | None = Field(
        None,
        description="End date (ISO format or business terms like 'today')",
    )
    statuses: list[str] | None = Field(
        None,
        description="Test statuses to include (default: excludes 'initialized' and 'cancelled')",
    )
    force_refresh_bugs: bool = Field(
        False,
        description="Bypass cache and fetch all bugs from API (slower but guaranteed fresh)",
    )
    output_file: str | None = Field(
        None,
        description="Optional file path to export report (for large products >100 tests)",
    )


@api.post("/api/reports/ebr", response_model=GenerateEBROutput)
async def generate_ebr_report_rest(
    request: Request,
    body: EBRReportRequest,
) -> Any:
    """Generate EBR (Executive Bug Report) for a product.

    Request body:
    - product_id: Product ID (required)
    - start_date: Start date filter (optional)
    - end_date: End date filter (optional)
    - statuses: Test statuses to include (optional)
    - force_refresh_bugs: Bypass cache (optional, default: false)
    - output_file: Export to file (optional, for large products)
    """
    # Get ServerContext from FastAPI app state
    server_ctx = get_server_context_from_request(request)

    # Create service
    service = get_service_from_server_context(server_ctx, MultiTestReportService)

    # Delegate to service
    result = await service.generate_ebr_report(
        product_id=body.product_id,
        start_date=body.start_date,
        end_date=body.end_date,
        statuses=body.statuses,
        force_refresh_bugs=body.force_refresh_bugs,
        output_file=body.output_file,
    )

    # Service returns dict that matches GenerateEBROutput structure
    return GenerateEBROutput(**result)


# Health Endpoint (Task 5)


@api.get("/health", response_model=HealthCheckOutput)
async def health_check_rest(request: Request) -> HealthCheckOutput:
    """Health check endpoint for monitoring.

    Returns:
        Health status, version, uptime, and database statistics.
    """
    try:
        # Get ServerContext
        server_ctx = get_server_context_from_request(request)
        cache = server_ctx["cache"]

        # Calculate uptime
        start_time = getattr(request.app.state, "start_time", monotonic())
        uptime_seconds = monotonic() - start_time

        # Get database stats (call cache methods directly)
        total_tests = await cache.count_tests()
        total_products = await cache.count_products()
        db_size_mb = await cache.get_db_size_mb()

        return HealthCheckOutput(
            status="healthy",
            version="1.0.0",
            uptime_seconds=round(uptime_seconds, 2),
            database=DatabaseInfo(
                connected=True,
                total_tests=total_tests,
                total_products=total_products,
                database_size_mb=db_size_mb,
            ),
        )
    except Exception as e:
        # Return unhealthy status if anything fails
        return HealthCheckOutput(
            status="unhealthy",
            version="1.0.0",
            error=str(e),
            database=DatabaseInfo(connected=False),
        )
