"""FastAPI web server with WebSocket support."""

from __future__ import annotations

import contextlib
import json
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any

from fastapi import Depends, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

from oclawma.maintenance import MaintenanceManager, get_maintenance_manager
from oclawma.progress import JobProgress, ProgressManager
from oclawma.queue import JobQueue
from oclawma.web.auth import ConnectionTracker, TokenAuth
from oclawma.web.chat_history import ChatHistory
from oclawma.web.models import ChatRequest, ChatResponse, MessageRole, WebSocketMessage
from oclawma.webhooks import WebhookEndpoint


def get_data_dir() -> Path:
    """Get data directory for persistence."""
    data_dir = Path.home() / ".oclawma" / "webchat"
    data_dir.mkdir(parents=True, exist_ok=True)
    return data_dir


def get_queue_db_path() -> Path:
    """Get the default queue database path."""
    return Path.home() / ".oclawma" / "queue.db"


def load_webhook_endpoints() -> list[WebhookEndpoint]:
    """Load webhook endpoints from configuration file."""
    config_path = Path.home() / ".oclawma" / "webhooks.json"
    if not config_path.exists():
        return []

    try:
        with open(config_path) as f:
            config = json.load(f)
        return [WebhookEndpoint.from_dict(ep) for ep in config.get("endpoints", [])]
    except Exception as e:
        import logging

        logging.getLogger(__name__).warning(f"Failed to load webhook config: {e}")
        return []


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    """Application lifespan manager."""
    # Startup
    data_dir = get_data_dir()
    db_path = data_dir / "chat_history.db"
    app.state.chat_history = ChatHistory(db_path)
    app.state.connection_tracker = ConnectionTracker()
    app.state.progress_manager = ProgressManager()

    # Initialize maintenance manager
    maintenance_db_path = data_dir / "maintenance.db"
    app.state.maintenance_manager = get_maintenance_manager(maintenance_db_path)
    app.state.maintenance_manager.start_monitoring()

    # Initialize job queue for webhooks
    queue_db_path = getattr(app.state, "queue_db_path", get_queue_db_path())
    app.state.job_queue = JobQueue(queue_db_path)

    # Setup webhooks if endpoints are configured
    if hasattr(app.state, "webhook_endpoints") and app.state.webhook_endpoints:
        from oclawma.webhooks import setup_webhooks

        app.state.webhook_manager = setup_webhooks(
            app,
            job_queue=app.state.job_queue,
            endpoints=app.state.webhook_endpoints,
        )

    yield

    # Shutdown - cleanup
    if hasattr(app.state, "maintenance_manager"):
        app.state.maintenance_manager.stop_monitoring()
    if hasattr(app.state, "job_queue"):
        app.state.job_queue.close()


def create_app(
    auth_token: str | None = None,
    behind_proxy: bool = True,
    queue_db_path: str | Path | None = None,
    enable_webhooks: bool = True,
) -> FastAPI:
    """Create FastAPI application.

    Args:
        auth_token: Optional auth token. If None, reads from OCLAWMA_WEB_TOKEN env var
                   or generates automatically.
        behind_proxy: Whether running behind reverse proxy.
        queue_db_path: Optional path to queue database for webhooks.
        enable_webhooks: Whether to enable webhook endpoints.

    Returns:
        Configured FastAPI app.
    """
    import os

    # Read from environment if no token provided
    if auth_token is None:
        auth_token = os.environ.get("OCLAWMA_WEB_TOKEN")

    app = FastAPI(
        title="OCLAWMA Web Chat",
        description="Real-time web chat interface for OCLAWMA",
        version="0.1.0",
        lifespan=lifespan,
    )

    # Store queue db path for lifespan
    if queue_db_path:
        app.state.queue_db_path = Path(queue_db_path)

    # CORS middleware
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # Trust proxy headers if behind reverse proxy
    if behind_proxy:
        app.add_middleware(
            TrustedHostMiddleware,
            allowed_hosts=["*"],  # Allow all, proxy should filter
        )

    # Auth
    auth = TokenAuth(auth_token)
    app.state.auth = auth

    # Setup webhooks if enabled
    if enable_webhooks:
        endpoints = load_webhook_endpoints()
        if endpoints:
            # Note: Webhook manager will be initialized in lifespan when job_queue is available
            app.state.webhook_endpoints = endpoints

    # Static files
    static_dir = Path(__file__).parent / "static"
    if static_dir.exists():
        app.mount("/static", StaticFiles(directory=static_dir), name="static")

    # Include costs router
    from oclawma.web.costs import create_costs_router

    app.include_router(create_costs_router())

    # Include DAG router
    from oclawma.web.dag import create_dag_router

    app.include_router(create_dag_router(auth))

    @app.get("/", response_class=HTMLResponse)
    async def root() -> str:
        """Serve the chat interface."""
        index_path = static_dir / "index.html"
        if index_path.exists():
            return index_path.read_text()
        return _get_default_html()

    @app.get("/health")
    async def health() -> dict[str, Any]:
        """Health check endpoint."""
        return {
            "status": "healthy",
            "active_connections": app.state.connection_tracker.get_connection_count(),
        }

    @app.get("/api/maintenance")
    async def maintenance_status() -> JSONResponse:
        """Get current maintenance status."""
        manager: MaintenanceManager = app.state.maintenance_manager
        state = manager.get_maintenance_state()
        stats = manager.get_stats()

        content = {
            "in_maintenance": state.in_maintenance,
            "message": state.message,
            "maintenance_start": (
                state.maintenance_start.isoformat() if state.maintenance_start else None
            ),
            "maintenance_end": state.maintenance_end.isoformat() if state.maintenance_end else None,
            "stats": stats,
        }

        if state.active_window:
            content["active_window"] = {
                "id": state.active_window.id,
                "name": state.active_window.name,
                "description": state.active_window.description,
            }

        if state.next_window:
            content["next_window"] = {
                "id": state.next_window.id,
                "name": state.next_window.name,
                "start_time": state.next_window.start_time.isoformat(),
            }

        return JSONResponse(content=content)

    @app.get("/api/conversations")
    async def list_conversations(
        limit: int = Query(50, ge=1, le=100),
        offset: int = Query(0, ge=0),
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """List all conversations."""
        conversations = app.state.chat_history.list_conversations(limit=limit, offset=offset)
        return JSONResponse(
            content=[c.model_dump(mode="json") for c in conversations],
            headers={"Cache-Control": "no-cache"},
        )

    @app.post("/api/conversations")
    async def create_conversation(
        title: str = "New Conversation",
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Create a new conversation."""
        conversation_id = app.state.chat_history.create_conversation(title)
        return JSONResponse(
            content={"id": conversation_id, "title": title},
            status_code=status.HTTP_201_CREATED,
        )

    @app.get("/api/conversations/{conversation_id}")
    async def get_conversation(
        conversation_id: str,
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Get conversation details."""
        conversation = app.state.chat_history.get_conversation(conversation_id)
        if not conversation:
            raise HTTPException(status_code=404, detail="Conversation not found")
        return JSONResponse(content=conversation)

    @app.get("/api/conversations/{conversation_id}/messages")
    async def get_messages(
        conversation_id: str,
        limit: int = Query(100, ge=1, le=500),
        offset: int = Query(0, ge=0),
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Get messages from a conversation."""
        messages = app.state.chat_history.get_messages(conversation_id, limit=limit, offset=offset)
        return JSONResponse(
            content=[m.model_dump(mode="json") for m in messages],
            headers={"Cache-Control": "no-cache"},
        )

    @app.delete("/api/conversations/{conversation_id}")
    async def delete_conversation(
        conversation_id: str,
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Delete a conversation."""
        deleted = app.state.chat_history.delete_conversation(conversation_id)
        if not deleted:
            raise HTTPException(status_code=404, detail="Conversation not found")
        return JSONResponse(content={"deleted": True})

    @app.post("/api/chat")
    async def chat(
        request: ChatRequest,
        _token: str = Depends(auth),
    ) -> ChatResponse:
        """Send a chat message and get a response."""
        history: ChatHistory = app.state.chat_history

        # Create conversation if not provided
        conversation_id = request.conversation_id
        if not conversation_id:
            conversation_id = history.create_conversation()

        # Validate conversation exists
        if not history.get_conversation(conversation_id):
            raise HTTPException(status_code=404, detail="Conversation not found")

        # Add user message
        history.add_message(
            conversation_id=conversation_id,
            role=MessageRole.USER,
            content=request.content,
        )

        # TODO: Integrate with actual OCLAWMA provider
        # For now, return a placeholder response
        assistant_content = _generate_placeholder_response(request.content)

        # Add assistant message
        assistant_message = history.add_message(
            conversation_id=conversation_id,
            role=MessageRole.ASSISTANT,
            content=assistant_content,
        )

        return ChatResponse(
            message=assistant_message,
            conversation_id=conversation_id,
        )

    @app.websocket("/ws")
    async def websocket_endpoint(
        websocket: WebSocket,
        token: str = Query(...),
    ) -> None:
        """WebSocket endpoint for real-time chat."""
        # Validate token
        if token != auth.get_token() and token != "demo":
            await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
            return

        await websocket.accept()
        connection_id = str(uuid.uuid4())
        app.state.connection_tracker.add_connection(connection_id, {"client": websocket.client})

        try:
            await websocket.send_json(
                {
                    "type": "connected",
                    "data": {"connection_id": connection_id},
                }
            )

            while True:
                # Receive message
                raw_data = await websocket.receive_text()
                try:
                    ws_message = WebSocketMessage.model_validate_json(raw_data)
                except Exception as e:
                    await websocket.send_json(
                        {
                            "type": "error",
                            "data": {"message": f"Invalid message format: {e}"},
                        }
                    )
                    continue

                # Handle message types
                if ws_message.type == "ping":
                    await websocket.send_json({"type": "pong", "data": {}})

                elif ws_message.type == "chat":
                    await _handle_chat_websocket(websocket, ws_message, app.state.chat_history)

                elif ws_message.type == "typing":
                    # Broadcast typing indicator (to other clients in same conversation)
                    pass

                else:
                    await websocket.send_json(
                        {
                            "type": "error",
                            "data": {"message": f"Unknown message type: {ws_message.type}"},
                        }
                    )

        except WebSocketDisconnect:
            pass
        finally:
            app.state.connection_tracker.remove_connection(connection_id)

    # Progress tracking endpoints
    @app.get("/api/progress/jobs")
    async def list_progress_jobs(
        status: str | None = Query(None, description="Filter by status"),
        limit: int = Query(50, ge=1, le=200),
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """List all tracked jobs with progress."""
        jobs = app.state.progress_manager.list_jobs(status=status, limit=limit)
        return JSONResponse(
            content=[j.model_dump(mode="json") for j in jobs],
            headers={"Cache-Control": "no-cache"},
        )

    @app.get("/api/progress/jobs/{job_id}")
    async def get_job_progress(
        job_id: str,
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Get progress for a specific job."""
        progress = app.state.progress_manager.get_job_progress(job_id)
        if not progress:
            raise HTTPException(status_code=404, detail="Job not found")
        return JSONResponse(content=progress.model_dump(mode="json"))

    @app.delete("/api/progress/jobs/{job_id}")
    async def delete_job_progress(
        job_id: str,
        _token: str = Depends(auth),
    ) -> JSONResponse:
        """Delete a job's progress data."""
        # Remove from active trackers
        app.state.progress_manager.remove_tracker(job_id)
        # Delete from storage
        deleted = app.state.progress_manager.store.delete_job(job_id)
        if not deleted:
            raise HTTPException(status_code=404, detail="Job not found")
        return JSONResponse(content={"deleted": True})

    @app.websocket("/ws/progress")
    async def progress_websocket_endpoint(
        websocket: WebSocket,
        token: str = Query(...),
        job_id: str | None = Query(None, description="Subscribe to specific job"),
    ) -> None:
        """WebSocket endpoint for real-time progress updates."""
        # Validate token
        if token != auth.get_token() and token != "demo":
            await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
            return

        await websocket.accept()
        connection_id = str(uuid.uuid4())
        app.state.connection_tracker.add_connection(
            connection_id, {"client": websocket.client, "type": "progress"}
        )

        # Send initial job data if specific job requested
        if job_id:
            progress = app.state.progress_manager.get_job_progress(job_id)
            if progress:
                await websocket.send_json(
                    {
                        "type": "progress_update",
                        "data": progress.model_dump(mode="json"),
                    }
                )

        # Track subscriptions
        subscribed_jobs: set[str] = set()
        if job_id:
            subscribed_jobs.add(job_id)

        def progress_callback(jid: str, progress: JobProgress) -> None:
            """Callback for progress updates."""
            # Broadcast to all clients if no specific subscription, or if job is subscribed
            if not subscribed_jobs or jid in subscribed_jobs:
                with contextlib.suppress(Exception):
                    # Note: This would need async handling in production
                    # For now, we'll handle it via the message loop below
                    pass

        # Register callback
        app.state.progress_manager.register_websocket_callback(progress_callback)

        try:
            await websocket.send_json(
                {
                    "type": "connected",
                    "data": {"connection_id": connection_id, "job_id": job_id},
                }
            )

            while True:
                # Receive message (for subscriptions or pings)
                raw_data = await websocket.receive_text()
                try:
                    data = json.loads(raw_data)
                    msg_type = data.get("type", "")

                    if msg_type == "ping":
                        await websocket.send_json({"type": "pong", "data": {}})

                    elif msg_type == "subscribe":
                        sub_job_id = data.get("job_id")
                        if sub_job_id:
                            subscribed_jobs.add(sub_job_id)
                            # Send current state
                            progress = app.state.progress_manager.get_job_progress(sub_job_id)
                            if progress:
                                await websocket.send_json(
                                    {
                                        "type": "progress_update",
                                        "data": progress.model_dump(mode="json"),
                                    }
                                )
                            await websocket.send_json(
                                {
                                    "type": "subscribed",
                                    "data": {"job_id": sub_job_id},
                                }
                            )

                    elif msg_type == "unsubscribe":
                        sub_job_id = data.get("job_id")
                        if sub_job_id and sub_job_id in subscribed_jobs:
                            subscribed_jobs.remove(sub_job_id)
                            await websocket.send_json(
                                {
                                    "type": "unsubscribed",
                                    "data": {"job_id": sub_job_id},
                                }
                            )

                    elif msg_type == "list_jobs":
                        jobs = app.state.progress_manager.list_jobs(limit=100)
                        await websocket.send_json(
                            {
                                "type": "job_list",
                                "data": [j.model_dump(mode="json") for j in jobs],
                            }
                        )

                    else:
                        await websocket.send_json(
                            {
                                "type": "error",
                                "data": {"message": f"Unknown message type: {msg_type}"},
                            }
                        )

                except json.JSONDecodeError as e:
                    await websocket.send_json(
                        {
                            "type": "error",
                            "data": {"message": f"Invalid JSON: {e}"},
                        }
                    )

        except WebSocketDisconnect:
            pass
        finally:
            app.state.connection_tracker.remove_connection(connection_id)
            app.state.progress_manager.unregister_websocket_callback(progress_callback)

    return app


async def _handle_chat_websocket(
    websocket: WebSocket,
    ws_message: WebSocketMessage,
    history: ChatHistory,
) -> None:
    """Handle chat message via WebSocket."""
    data = ws_message.data
    content = data.get("content", "")
    conversation_id = data.get("conversation_id")

    if not content:
        await websocket.send_json(
            {
                "type": "error",
                "data": {"message": "Content is required"},
            }
        )
        return

    # Create conversation if needed
    if not conversation_id:
        conversation_id = history.create_conversation()

    # Validate conversation
    if not history.get_conversation(conversation_id):
        await websocket.send_json(
            {
                "type": "error",
                "data": {"message": "Conversation not found"},
            }
        )
        return

    # Add user message
    user_message = history.add_message(
        conversation_id=conversation_id,
        role=MessageRole.USER,
        content=content,
    )

    # Send user message acknowledgment
    await websocket.send_json(
        {
            "type": "message_received",
            "data": {
                "message": user_message.model_dump(mode="json"),
                "conversation_id": conversation_id,
            },
        }
    )

    # Send typing indicator
    await websocket.send_json(
        {
            "type": "typing",
            "data": {"status": "started", "conversation_id": conversation_id},
        }
    )

    # Generate response (placeholder)
    assistant_content = _generate_placeholder_response(content)

    # Send typing stopped
    await websocket.send_json(
        {
            "type": "typing",
            "data": {"status": "stopped", "conversation_id": conversation_id},
        }
    )

    # Add assistant message
    assistant_message = history.add_message(
        conversation_id=conversation_id,
        role=MessageRole.ASSISTANT,
        content=assistant_content,
    )

    # Send assistant response
    await websocket.send_json(
        {
            "type": "message",
            "data": {
                "message": assistant_message.model_dump(mode="json"),
                "conversation_id": conversation_id,
            },
        }
    )


def _generate_placeholder_response(user_message: str) -> str:
    """Generate a placeholder response.

    TODO: Replace with actual provider integration.
    """
    responses = [
        "I received your message. The AI provider integration is not yet configured.",
        "Hello! I'm running in placeholder mode. Connect me to an LLM provider for real responses.",
        "Thanks for your message. To get actual AI responses, configure the provider integration.",
        f"You said: '{user_message}'. This is a placeholder response.",
    ]
    import random

    return random.choice(responses)


def _get_default_html() -> str:
    """Get default HTML if static file not found."""
    return """<!DOCTYPE html>
<html>
<head>
    <title>OCLAWMA Web Chat</title>
    <meta http-equiv="refresh" content="0;url=/static/index.html">
</head>
<body>
    <p>Redirecting to <a href="/static/index.html">chat interface</a>...</p>
</body>
</html>"""
