"""FastAPI application factory."""

from pathlib import Path
from typing import Any

# Base directory for server resources
SERVER_DIR = Path(__file__).parent
TEMPLATES_DIR = SERVER_DIR / "templates"
STATIC_DIR = SERVER_DIR / "static"


def _check_dependencies() -> None:
    """Check if server dependencies are installed."""
    missing = []
    try:
        import fastapi  # noqa: F401
    except ImportError:
        missing.append("fastapi")

    try:
        import uvicorn  # noqa: F401
    except ImportError:
        missing.append("uvicorn[standard]")

    try:
        import websockets  # noqa: F401
    except ImportError:
        missing.append("websockets")

    try:
        import jinja2  # noqa: F401
    except ImportError:
        missing.append("jinja2")

    if missing:
        msg = (
            f"Missing server dependencies: {', '.join(missing)}\n"
            "Install with: pip install ieum[server]"
        )
        raise ImportError(msg)


def create_app(config=None):
    """Create and configure the FastAPI application."""
    from contextlib import asynccontextmanager

    from fastapi import Depends, FastAPI, Request, WebSocket
    from fastapi.middleware.cors import CORSMiddleware
    from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
    from fastapi.staticfiles import StaticFiles
    from starlette.middleware.base import BaseHTTPMiddleware

    # Import config lazily
    from ieum.server.auth import (
        init_auth,
        is_auth_disabled,
        is_request_authenticated,
        get_auth_token,
        get_auth_token_ttl_seconds,
        get_websocket_user_id,
        validate_ws_token,
        PUBLIC_PATHS,
    )
    from ieum.server.config import default_config as _default_config
    from ieum.server.rate_limit import RateLimitMiddleware
    from ieum.server.session_policy import SessionPolicyResolver
    from ieum.server.routes import (
        checkpoints, cost, files, git, health, memory, models, plan, sessions, skills, tasks, verify,
    )
    from ieum.server.services.session_service import SessionService
    from ieum.server.services.task_service import TaskService
    from ieum.server.websocket.manager import WebSocketManager

    if config is None:
        config = _default_config

    # Create services
    session_service = SessionService(
        max_sessions=config.max_sessions,
        session_timeout_minutes=config.session_timeout_minutes,
    )
    session_policy_resolver = SessionPolicyResolver(config)
    ws_manager = WebSocketManager()
    task_service = TaskService(max_concurrent=3)
    task_service._session_service = session_service
    ws_manager._task_service = task_service

    # Wire task service notifications through the WebSocket manager
    async def _task_notify(session_id: str, event_type: str, data: dict) -> None:
        from ieum.server.websocket.protocol import WSMessage, MessageType
        try:
            msg_type = MessageType(event_type)
        except ValueError:
            return
        await ws_manager.broadcast(session_id, WSMessage(type=msg_type, data=data))

    task_service.set_notify_callback(_task_notify)

    # Initialize authentication
    auth_token = init_auth()
    auth_token_ttl_seconds = get_auth_token_ttl_seconds()

    def _is_forwarded_https(headers: dict[str, str] | Any) -> bool:
        """Check whether proxy headers indicate HTTPS transport."""
        forwarded_proto = (headers.get("x-forwarded-proto") or "").split(",")[0].strip().lower()
        return forwarded_proto == "https"

    @asynccontextmanager
    async def lifespan(app: FastAPI):
        """Application lifespan handler."""
        import asyncio
        import logging

        _logger = logging.getLogger(__name__)

        # Startup: Display auth info
        if auth_token is not None:
            print(f"\n{'=' * 60}")
            print("  IEUM Authentication Token:")
            print(f"  {auth_token}")
            print(f"  Token saved to: ~/.ieum/auth_token")
            print(f"{'=' * 60}\n")

        # Startup: Load persisted sessions from SQLite
        await session_service.load_persisted_sessions()

        # Startup: Start background task workers
        await task_service.start(worker_count=2)

        # A6: Start background cleanup scheduler for expired sessions
        cleanup_task = asyncio.create_task(
            session_service.cleanup_expired_sessions_loop()
        )
        _logger.info("Started session cleanup scheduler")

        try:
            yield
        finally:
            # Shutdown: Cancel the cleanup scheduler
            cleanup_task.cancel()
            try:
                await cleanup_task
            except asyncio.CancelledError:
                pass

            # Shutdown: Stop background task workers
            await task_service.stop()

            # Shutdown: Cancel all pending WebSocket background tasks
            await ws_manager.shutdown()

            # Shutdown: Close all sessions
            await session_service.close_all()

    app = FastAPI(
        title="IEUM API",
        description="AI Coding Agent API Server",
        version="0.0.1",
        lifespan=lifespan,
    )

    # Store config in app state
    app.state.config = config
    app.state.session_service = session_service
    app.state.session_policy_resolver = session_policy_resolver
    app.state.ws_manager = ws_manager
    app.state.task_service = task_service
    # Allow session service to clean up WebSocket data on session deletion
    session_service._ws_manager = ws_manager

    # ------------------------------------------------------------------
    # S6: Content-Security-Policy middleware
    # ------------------------------------------------------------------
    class CSPMiddleware(BaseHTTPMiddleware):
        """Add Content-Security-Policy headers to all responses."""

        async def dispatch(self, request: Request, call_next):
            response = await call_next(request)
            csp_directives = [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline'",
                "style-src 'self' 'unsafe-inline'",
                "connect-src 'self' ws: wss:",
                "img-src 'self' data:",
                "font-src 'self' data:",
                "object-src 'none'",
                "base-uri 'self'",
                "form-action 'self'",
                "frame-ancestors 'none'",
            ]
            response.headers["Content-Security-Policy"] = "; ".join(csp_directives)
            response.headers["X-Content-Type-Options"] = "nosniff"
            response.headers["X-Frame-Options"] = "DENY"
            response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
            return response

    app.add_middleware(CSPMiddleware)

    # ------------------------------------------------------------------
    # TLS enforcement middleware
    # ------------------------------------------------------------------
    class TLSRequiredMiddleware(BaseHTTPMiddleware):
        """Reject non-TLS requests when TLS is required by configuration."""

        async def dispatch(self, request: Request, call_next):
            if not config.tls_required:
                return await call_next(request)

            is_https = request.url.scheme == "https"
            if not is_https and config.trust_proxy_tls:
                is_https = _is_forwarded_https(request.headers)

            if not is_https:
                return JSONResponse(
                    status_code=426,
                    content={
                        "detail": "TLS is required. Use HTTPS or configure a trusted TLS proxy.",
                    },
                )
            return await call_next(request)

    app.add_middleware(TLSRequiredMiddleware)

    # ------------------------------------------------------------------
    # S3: Authentication middleware
    # ------------------------------------------------------------------
    class AuthMiddleware(BaseHTTPMiddleware):
        """Enforce token authentication on all non-public routes."""

        async def dispatch(self, request: Request, call_next):
            path = request.url.path.rstrip("/")

            # Allow public paths
            if path in PUBLIC_PATHS or path == "":
                return await call_next(request)

            # Allow static assets without auth
            if path.startswith("/static"):
                return await call_next(request)

            # Skip auth if disabled
            if is_auth_disabled():
                return await call_next(request)

            # Validate token
            if not is_request_authenticated(request):
                # For API requests, return 401 JSON
                if path.startswith("/api"):
                    return JSONResponse(
                        status_code=401,
                        content={"detail": "Invalid or missing authentication token"},
                    )
                # For page requests, redirect to login
                return RedirectResponse(url="/login", status_code=303)

            return await call_next(request)

    app.add_middleware(AuthMiddleware)

    # ------------------------------------------------------------------
    # S4: CORS middleware (restrictive defaults)
    # ------------------------------------------------------------------
    app.add_middleware(
        CORSMiddleware,
        allow_origins=config.cors_origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # ------------------------------------------------------------------
    # Rate-limit middleware
    # ------------------------------------------------------------------
    if config.rate_limit_enabled:
        app.add_middleware(
            RateLimitMiddleware,
            max_requests=config.rate_limit_max_requests,
            window_seconds=config.rate_limit_window_seconds,
        )

    # ------------------------------------------------------------------
    # Routes
    # ------------------------------------------------------------------

    # Register API routes
    app.include_router(health.router, prefix="/api", tags=["health"])
    app.include_router(sessions.router, prefix="/api/sessions", tags=["sessions"])
    app.include_router(files.router, prefix="/api/sessions", tags=["files"])
    app.include_router(models.router, prefix="/api", tags=["models"])
    app.include_router(skills.router, tags=["skills"])
    app.include_router(tasks.router, prefix="/api/sessions", tags=["tasks"])
    app.include_router(git.router, prefix="/api/sessions", tags=["git"])
    app.include_router(verify.router, prefix="/api/sessions", tags=["verify"])
    app.include_router(checkpoints.router, prefix="/api/sessions", tags=["checkpoints"])
    app.include_router(memory.router, prefix="/api/sessions", tags=["memory"])
    app.include_router(plan.router, prefix="/api/sessions", tags=["plan"])
    app.include_router(cost.router, prefix="/api/sessions", tags=["cost"])

    # ------------------------------------------------------------------
    # Authentication endpoints (S3)
    # ------------------------------------------------------------------

    @app.get("/login")
    async def login_page(request: Request):
        """Serve a simple login page for token entry."""
        if is_auth_disabled() or is_request_authenticated(request):
            return RedirectResponse(url="/", status_code=303)

        login_html = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IEUM - Login</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #1a1d21; color: #abb2bf;
            display: flex; align-items: center; justify-content: center;
            min-height: 100vh;
        }
        .login-card {
            background: #21252b; border: 1px solid #3a3f4b;
            border-radius: 8px; padding: 2rem; max-width: 400px; width: 100%;
        }
        h1 { color: #fff; font-size: 1.5rem; margin-bottom: 0.5rem; }
        p { color: #5c6370; font-size: 0.875rem; margin-bottom: 1.5rem; }
        label { display: block; font-size: 0.875rem; margin-bottom: 0.5rem; color: #abb2bf; }
        input[type="text"] {
            width: 100%; padding: 0.75rem; background: #1a1d21;
            border: 1px solid #3a3f4b; border-radius: 4px; color: #abb2bf;
            font-size: 0.875rem; font-family: monospace;
        }
        input[type="text"]:focus { outline: none; border-color: #4d8fcc; }
        button {
            width: 100%; padding: 0.75rem; background: #4d8fcc;
            border: none; border-radius: 4px; color: #fff; font-size: 0.875rem;
            cursor: pointer; margin-top: 1rem;
        }
        button:hover { background: #3a7abd; }
        .error { color: #e06c75; font-size: 0.8rem; margin-top: 0.5rem; display: none; }
        .hint { color: #5c6370; font-size: 0.75rem; margin-top: 1rem; }
    </style>
</head>
<body>
    <div class="login-card">
        <h1>IEUM</h1>
        <p>Enter the authentication token displayed in the server console.</p>
        <form id="authForm">
            <label for="token">Authentication Token</label>
            <input type="text" id="token" name="token" placeholder="Paste your token here"
                   autocomplete="off" autofocus>
            <div class="error" id="errorMsg">Invalid token. Please try again.</div>
            <button type="submit">Authenticate</button>
        </form>
        <p class="hint">Token is saved in ~/.ieum/auth_token</p>
    </div>
    <script>
        document.getElementById('authForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const token = document.getElementById('token').value.trim();
            if (!token) return;
            try {
                const resp = await fetch('/auth', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({token: token})
                });
                if (resp.ok) {
                    window.location.href = '/';
                } else {
                    document.getElementById('errorMsg').style.display = 'block';
                }
            } catch (err) {
                document.getElementById('errorMsg').style.display = 'block';
            }
        });
    </script>
</body>
</html>"""
        return HTMLResponse(content=login_html)

    @app.post("/auth")
    async def authenticate(request: Request):
        """Validate token and set authentication cookie."""
        import secrets as _secrets
        body = await request.json()
        token = body.get("token", "")

        expected = get_auth_token()
        if expected is None or not _secrets.compare_digest(token, expected):
            return JSONResponse(
                status_code=401,
                content={"detail": "Invalid token"},
            )

        response = JSONResponse(content={"success": True})
        response.set_cookie(
            key="ieum_token",
            value=token,
            httponly=True,
            samesite="strict",
            secure=config.tls_required,
            max_age=auth_token_ttl_seconds,
        )
        return response

    # ------------------------------------------------------------------
    # WebSocket endpoints (with auth)
    # ------------------------------------------------------------------

    @app.websocket("/ws/test")
    async def websocket_test(websocket: WebSocket) -> None:
        """WebSocket test/echo endpoint."""
        if config.tls_required:
            ws_scheme = (websocket.url.scheme or "").lower()
            is_tls = ws_scheme == "wss"
            if not is_tls and config.trust_proxy_tls:
                is_tls = _is_forwarded_https(websocket.headers)
            if not is_tls:
                await websocket.close(code=4003, reason="TLS required")
                return

        # Validate auth on WebSocket handshake
        if not await validate_ws_token(websocket):
            await websocket.close(code=4001, reason="Authentication required")
            return
        await websocket.accept()
        await websocket.send_json({"type": "connected"})
        try:
            while True:
                data = await websocket.receive_json()
                await websocket.send_json({"type": "echo", "data": data})
        except Exception:
            pass

    @app.websocket("/ws/sessions/{session_id}")
    async def websocket_endpoint(websocket: WebSocket, session_id: str) -> None:
        """WebSocket session endpoint with authentication."""
        if config.tls_required:
            ws_scheme = (websocket.url.scheme or "").lower()
            is_tls = ws_scheme == "wss"
            if not is_tls and config.trust_proxy_tls:
                is_tls = _is_forwarded_https(websocket.headers)
            if not is_tls:
                await websocket.close(code=4003, reason="TLS required")
                return

        # Validate auth on WebSocket handshake
        if not await validate_ws_token(websocket):
            await websocket.close(code=4001, reason="Authentication required")
            return
        try:
            ws_user_id = get_websocket_user_id(
                websocket,
                required=config.require_user_identity,
            )
        except ValueError:
            await websocket.close(code=4400, reason="Missing user identity")
            return

        if app.state.session_service.get_session(session_id, user_id=ws_user_id) is None:
            await websocket.close(code=4004, reason="Session not found")
            return
        last_seq_raw = websocket.query_params.get("last_seq")
        try:
            last_seq = int(last_seq_raw) if last_seq_raw is not None else None
        except ValueError:
            last_seq = None
        await app.state.ws_manager.handle_connection(
            websocket,
            session_id,
            app.state.session_service,
            user_id=ws_user_id,
            last_seq=last_seq,
        )

    # ------------------------------------------------------------------
    # i18n translations API
    # ------------------------------------------------------------------

    @app.get("/api/i18n/{locale}")
    async def get_translations(locale: str):
        """Serve translation strings for the given locale."""
        from ieum.i18n import get_supported_locales, get_translations_dict

        if locale not in get_supported_locales():
            locale = "en"
        return JSONResponse(get_translations_dict(locale))

    @app.get("/api/i18n")
    async def get_default_translations():
        """Serve translation strings for the current server locale."""
        from ieum.i18n import get_locale, get_translations_dict

        return JSONResponse(get_translations_dict(get_locale()))

    # ------------------------------------------------------------------
    # Static files and index
    # ------------------------------------------------------------------

    if STATIC_DIR.exists():
        app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")

    if TEMPLATES_DIR.exists():
        @app.get("/")
        async def serve_index(request: Request):
            """Serve main page (redirect to login if not authenticated)."""
            if not is_auth_disabled() and not is_request_authenticated(request):
                return RedirectResponse(url="/login", status_code=303)
            return FileResponse(TEMPLATES_DIR / "index.html")

    return app


def run_server(config=None):
    """Run the API server."""
    _check_dependencies()

    import uvicorn
    from ieum.server.config import default_config as _default_config

    if config is None:
        config = _default_config

    if config.remote_only and config.host in {"127.0.0.1", "localhost"}:
        msg = (
            "Remote-only mode is enabled but server host is loopback. "
            "Use --host 0.0.0.0 (or a non-loopback interface) for centralized deployment."
        )
        raise RuntimeError(msg)

    if config.tls_required and not config.trust_proxy_tls:
        if not config.ssl_certfile or not config.ssl_keyfile:
            msg = (
                "TLS is required but SSL certificate/key are not configured. "
                "Set IEUM_SSL_CERTFILE and IEUM_SSL_KEYFILE, "
                "or enable IEUM_TRUST_PROXY_TLS=true behind a TLS terminator."
            )
            raise RuntimeError(msg)

    app = create_app(config)

    uvicorn_kwargs = dict(
        app=app,
        host=config.host,
        port=config.port,
        log_level=config.log_level,
    )
    if config.ssl_certfile and config.ssl_keyfile:
        uvicorn_kwargs["ssl_certfile"] = config.ssl_certfile
        uvicorn_kwargs["ssl_keyfile"] = config.ssl_keyfile

    uvicorn.run(**uvicorn_kwargs)
