"""MCP Server for Platform-2Step.

This server enables AI agents to interact with AgendaPro's Platform API
through a human-in-the-loop confirmation system.

SECURITY: Mutations create pending operations that require human confirmation.
The MCP cannot confirm operations - only create pending ones.
"""

import logging
import sys
from contextlib import asynccontextmanager
from typing import Any

import jsonschema
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, TextContent, Tool

from .auth import AuthClient
from .client import Platform2StepClient
from .config import Settings, get_settings
from .tools import (
    ANALYTICS_TOOLS,
    BATCH_TOOLS,
    BOOKING_TOOLS,
    CATEGORY_TOOLS,
    LOCATION_TOOLS,
    MEMBERSHIP_TOOLS,
    PROVIDER_TOOLS,
    SERVICE_TOOLS,
    USER_TOOLS,
    handle_analytics_tool,
    handle_batch_tool,
    handle_booking_tool,
    handle_category_tool,
    handle_location_tool,
    handle_membership_tool,
    handle_provider_tool,
    handle_service_tool,
    handle_user_tool,
)

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr,
)
logger = logging.getLogger("platform-2step-mcp")

# Global client instance
_client: Platform2StepClient | None = None
_auth_client: AuthClient | None = None


def create_auth_client(settings: Settings) -> AuthClient:
    """Create an AuthClient for device flow authentication.

    Args:
        settings: Application settings.

    Returns:
        AuthClient configured with the BFF URL.
    """
    return AuthClient(
        bff_url=settings.bff_url,
    )


def get_client(settings: Settings, auth_client: AuthClient) -> Platform2StepClient:
    """Get or create the Platform2Step client.

    Args:
        settings: Application settings.
        auth_client: AuthClient for device flow authentication.

    Returns:
        Platform2StepClient instance.
    """
    global _client
    if _client is None:
        _client = Platform2StepClient(
            base_url=settings.api_url,
            auth_client=auth_client,
        )
    return _client


# All available tools
ALL_TOOLS: list[Tool] = (
    CATEGORY_TOOLS
    + SERVICE_TOOLS
    + PROVIDER_TOOLS
    + BOOKING_TOOLS
    + LOCATION_TOOLS
    + MEMBERSHIP_TOOLS
    + USER_TOOLS
    + BATCH_TOOLS
    + ANALYTICS_TOOLS
)

# Tool name to handler mapping
TOOL_HANDLERS: dict[str, tuple[str, Any]] = {}

# Build handler mapping
for tool in CATEGORY_TOOLS:
    TOOL_HANDLERS[tool.name] = ("category", handle_category_tool)
for tool in SERVICE_TOOLS:
    TOOL_HANDLERS[tool.name] = ("service", handle_service_tool)
for tool in BOOKING_TOOLS:
    TOOL_HANDLERS[tool.name] = ("booking", handle_booking_tool)
for tool in LOCATION_TOOLS:
    TOOL_HANDLERS[tool.name] = ("location", handle_location_tool)
for tool in MEMBERSHIP_TOOLS:
    TOOL_HANDLERS[tool.name] = ("membership", handle_membership_tool)
for tool in PROVIDER_TOOLS:
    TOOL_HANDLERS[tool.name] = ("provider", handle_provider_tool)
for tool in USER_TOOLS:
    TOOL_HANDLERS[tool.name] = ("user", handle_user_tool)
for tool in BATCH_TOOLS:
    TOOL_HANDLERS[tool.name] = ("batch", handle_batch_tool)
for tool in ANALYTICS_TOOLS:
    TOOL_HANDLERS[tool.name] = ("analytics", handle_analytics_tool)

# Schema lookup for coercion and manual validation
TOOL_SCHEMAS: dict[str, dict] = {tool.name: tool.inputSchema for tool in ALL_TOOLS}


def _coerce_arguments(tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
    """Coerce string arguments to their schema-declared types.

    LLMs sometimes serialize integers as strings (e.g. "9479" instead of 9479).
    This uses the tool's inputSchema to coerce values before validation.
    """
    schema = TOOL_SCHEMAS.get(tool_name)
    if not schema:
        return arguments

    properties = schema.get("properties", {})
    coerced = dict(arguments)

    for key, prop in properties.items():
        if key in coerced and prop.get("type") == "integer" and isinstance(coerced[key], str):
            try:
                coerced[key] = int(coerced[key])
            except (ValueError, TypeError):
                raise jsonschema.ValidationError(
                    f"'{coerced[key]}' is not of type 'integer'"
                )

    return coerced


ANALYTICS_SCHEMA_RESOURCE = """# Analytics Database Schema

## Tables

### bookings
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| client_id | INTEGER | FK → clients.id |
| service_id | INTEGER | FK → services.id |
| provider_id | INTEGER | FK → providers.id |
| location_id | INTEGER | FK → locations.id |
| start_time | TIMESTAMPTZ | Booking start |
| end_time | TIMESTAMPTZ | Booking end |
| status | VARCHAR | pending, confirmed, completed, cancelled, no_show |
| price | NUMERIC(12,2) | Booking price |
| notes | TEXT | Optional notes |
| created_at | TIMESTAMPTZ | Creation timestamp |

### services
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Service name |
| category_id | INTEGER | FK → categories.id |
| duration | INTEGER | Duration in minutes |
| price | NUMERIC(12,2) | Service price |
| active | BOOLEAN | Is active? |
| company_id | INTEGER | Company owner |

### categories
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Category name |
| "order" | INTEGER | Sort order (quoted, reserved word) |
| company_id | INTEGER | Company owner |

### providers
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Provider name |
| email | VARCHAR | Email |
| active | BOOLEAN | Is active? |
| company_id | INTEGER | Company owner |

### locations
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Location name |
| address | TEXT | Physical address |
| phone | VARCHAR | Phone number |
| timezone | VARCHAR | Timezone (e.g., America/Santiago) |
| company_id | INTEGER | Company owner |

### transactions
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| booking_id | INTEGER | FK → bookings.id |
| amount | NUMERIC(12,2) | Transaction amount |
| status | VARCHAR | pending, completed, refunded, failed |
| payment_method | VARCHAR | Payment method |
| paid_at | TIMESTAMPTZ | Payment timestamp |
| created_at | TIMESTAMPTZ | Creation timestamp |

### clients
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Client name |
| email | VARCHAR | Email |
| phone | VARCHAR | Phone number |
| created_at | TIMESTAMPTZ | Registration date |

## Relationships

```
categories ──< services ──< bookings >── clients
                              │   │
                              │   └──> providers
                              │   └──> locations
                              └──< transactions
```

## Example Queries

### Revenue Analysis
```sql
-- Monthly revenue trend
SELECT DATE_TRUNC('month', t.paid_at) AS month,
       SUM(t.amount) AS revenue,
       COUNT(*) AS transactions
FROM transactions t
WHERE t.status = 'completed' AND t.paid_at > NOW() - INTERVAL '6 months'
GROUP BY month ORDER BY month;
```

### Client Insights
```sql
-- Top clients by spend
SELECT c.name, c.email, SUM(t.amount) AS total_spent, COUNT(*) AS visits
FROM clients c
JOIN bookings b ON b.client_id = c.id
JOIN transactions t ON t.booking_id = b.id
WHERE t.status = 'completed'
GROUP BY c.name, c.email
ORDER BY total_spent DESC LIMIT 10;
```

### No-Show Analysis
```sql
-- No-show rate by provider
SELECT p.name,
       COUNT(*) AS total,
       COUNT(CASE WHEN b.status = 'no_show' THEN 1 END) AS no_shows,
       ROUND(COUNT(CASE WHEN b.status = 'no_show' THEN 1 END)::numeric / COUNT(*) * 100, 1) AS no_show_pct
FROM providers p
JOIN bookings b ON b.provider_id = p.id
WHERE b.start_time > NOW() - INTERVAL '90 days'
GROUP BY p.name ORDER BY no_show_pct DESC;
```

### Service Performance
```sql
-- Service utilization and revenue
SELECT s.name, cat.name AS category,
       COUNT(b.id) AS bookings, SUM(t.amount) AS revenue,
       ROUND(AVG(t.amount), 2) AS avg_ticket
FROM services s
JOIN categories cat ON s.category_id = cat.id
LEFT JOIN bookings b ON b.service_id = s.id AND b.start_time > NOW() - INTERVAL '30 days'
LEFT JOIN transactions t ON t.booking_id = b.id AND t.status = 'completed'
WHERE s.active = true
GROUP BY s.name, cat.name ORDER BY revenue DESC NULLS LAST;
```

### Occupancy
```sql
-- Daily booking count by location
SELECT l.name AS location,
       DATE(b.start_time) AS day,
       COUNT(*) AS bookings
FROM locations l
JOIN bookings b ON b.location_id = l.id
WHERE b.start_time BETWEEN NOW() - INTERVAL '7 days' AND NOW()
GROUP BY l.name, day ORDER BY l.name, day;
```

## Data Freshness

- Data is synced periodically from the Platform API
- Check `data_as_of` and `sync_lag_seconds` in query responses
- Use `freshness_policy: "require_fresh"` if you need up-to-date data
- Typical sync lag: 5-30 minutes
"""


@asynccontextmanager
async def create_server(settings: Settings, auth_client: AuthClient):
    """Create and configure the MCP server.

    Args:
        settings: Application settings.
        auth_client: AuthClient for device flow authentication.

    Yields:
        Configured MCP Server instance.
    """
    server = Server("platform-2step")
    client = get_client(settings, auth_client)

    @server.list_resources()
    async def list_resources() -> list[Resource]:
        """List available resources."""
        return [
            Resource(
                uri="platform://analytics/schema-and-examples",
                name="Analytics DB Schema & Example Queries",
                description="Schema, relationships, and 10+ example queries for the analytics database",
                mimeType="text/plain",
            ),
        ]

    @server.read_resource()
    async def read_resource(uri: str) -> str:
        """Read a resource by URI."""
        if str(uri) == "platform://analytics/schema-and-examples":
            return ANALYTICS_SCHEMA_RESOURCE
        raise ValueError(f"Unknown resource: {uri}")

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        """List all available tools."""
        return ALL_TOOLS

    @server.call_tool(validate_input=False)
    async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
        """Handle tool calls with centralized argument coercion.

        Disables SDK-level schema validation so we can coerce LLM-provided
        string arguments to integers before re-validating against the schema.
        """
        logger.info(f"Tool call: {name} with arguments: {arguments}")

        if name not in TOOL_HANDLERS:
            logger.error(f"Unknown tool: {name}")
            return [TextContent(type="text", text=f"Unknown tool: {name}")]

        handler_type, handler = TOOL_HANDLERS[name]

        try:
            coerced_args = _coerce_arguments(name, arguments)

            schema = TOOL_SCHEMAS.get(name)
            if schema:
                jsonschema.validate(instance=coerced_args, schema=schema)

            result = await handler(name, coerced_args, client)
            logger.info(f"Tool {name} completed successfully")
            return result
        except jsonschema.ValidationError as e:
            logger.warning(f"Input validation error for {name}: {e.message}")
            return [TextContent(type="text", text=f"Input validation error: {e.message}")]
        except Exception as e:
            logger.exception(f"Error in tool {name}: {e}")
            return [TextContent(type="text", text=f"Error: {str(e)}")]

    try:
        yield server
    finally:
        await client.close()


def main() -> None:
    """Main entry point for the MCP server."""
    import asyncio

    # Load and validate settings
    try:
        settings = get_settings()
    except Exception as e:
        print(f"Configuration error: {e}", file=sys.stderr)
        print(
            "\nSet PLATFORM_2STEPS_BFF_URL environment variable:",
            file=sys.stderr,
        )
        print(
            "  export PLATFORM_2STEPS_BFF_URL=https://ap-api.agendaprodev.com/platform-2steps-bff",
            file=sys.stderr,
        )
        sys.exit(1)

    # Configure log level for all loggers
    log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
    logging.getLogger().setLevel(log_level)
    logger.setLevel(log_level)

    # Explicitly configure HTTP debug loggers
    if settings.DEBUG_HTTP:
        import os
        from pathlib import Path

        # Set up file handler for HTTP debug logs
        http_log_dir = Path.home() / ".platform-mcp" / "logs"
        http_log_dir.mkdir(parents=True, exist_ok=True)
        http_log_file = http_log_dir / "http_debug.log"

        file_handler = logging.FileHandler(http_log_file, mode="a")
        file_handler.setLevel(logging.DEBUG)
        file_handler.setFormatter(logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        ))

        # Add file handler to HTTP loggers
        http_client_logger = logging.getLogger("platform_2step_mcp.client.http")
        http_auth_logger = logging.getLogger("platform_2step_mcp.auth.client.http")
        http_client_logger.setLevel(logging.DEBUG)
        http_auth_logger.setLevel(logging.DEBUG)
        http_client_logger.addHandler(file_handler)
        http_auth_logger.addHandler(file_handler)

        logger.info(f"HTTP debug logging enabled (DEBUG_HTTP=true)")
        logger.info(f"HTTP logs written to: {http_log_file}")

    logger.info("Starting Platform-2Step MCP server")
    logger.info(f"API URL: {settings.api_url}")
    logger.info(f"BFF URL: {settings.bff_url}")

    # Create auth client for device flow
    auth_client = create_auth_client(settings)

    # Check for cached tokens (don't trigger interactive auth in server mode)
    logger.info("Checking for cached authentication tokens...")
    tokens = auth_client.storage.load()
    if tokens is None or tokens.is_expired():
        logger.error("Not authenticated or token expired")
        print(
            "\nError: Not authenticated. Run 'platform-mcp-auth login' first.",
            file=sys.stderr,
        )
        print(
            "\nThe MCP server runs non-interactively and cannot perform",
            file=sys.stderr,
        )
        print(
            "device flow authentication. Use the auth CLI to authenticate:",
            file=sys.stderr,
        )
        print(
            "\n  platform-mcp-auth login",
            file=sys.stderr,
        )
        print(
            "\nThen restart the MCP server.",
            file=sys.stderr,
        )
        sys.exit(1)
    logger.info("Using cached authentication tokens")

    async def run():
        async with create_server(settings, auth_client) as server:
            async with stdio_server() as (read_stream, write_stream):
                await server.run(
                    read_stream,
                    write_stream,
                    server.create_initialization_options(),
                )

    asyncio.run(run())


if __name__ == "__main__":
    main()
