"""
Standalone UI server for PyCharter.

Serves the built Next.js static files and proxies API requests to the PyCharter API.
Supports both installed package (pre-built static files) and development mode.
"""

from __future__ import annotations

import importlib.util
import json
import os
import sys
from pathlib import Path

import httpx
import uvicorn
from fastapi import FastAPI, Request, Response
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles


def find_ui_static_files() -> Path | None:
    """
    Find UI static files in multiple locations (package, source, etc.).

    Priority:
    1. Installed package location (pycharter/ui/static/)
    2. Source location (ui/static/)
    3. Next.js out directory (ui/out/)

    Returns:
        Path to static directory if found, None otherwise
    """
    # Try to find static files in installed package (src layout: pycharter/__file__ is package dir)
    try:
        import pycharter

        pycharter_path = Path(pycharter.__file__).parent
        package_static = pycharter_path / "ui" / "static"
        if package_static.exists() and (package_static / "index.html").exists():
            return package_static
    except (ImportError, AttributeError):
        pass

    # Try source locations (development; __file__ is pycharter/ui/server.py)
    possible_paths = [
        Path(__file__).parent / "static",  # pycharter/ui/static/
        Path(__file__).parent / "out",  # pycharter/ui/out/ (Next.js export)
        Path.cwd() / "src" / "pycharter" / "ui" / "static",
        Path.cwd() / "ui" / "static",
    ]

    for static_path in possible_paths:
        if static_path.exists() and (static_path / "index.html").exists():
            return static_path

    return None


def get_static_dir() -> Path:
    """Get the directory containing built static files."""
    static_dir = find_ui_static_files()

    if static_dir:
        return static_dir

    # Provide helpful error message
    raise FileNotFoundError(
        "No built UI found.\n\n"
        "If you installed from pip:\n"
        "  The UI static files should be included in the package.\n"
        "  If they're missing, please report this issue.\n\n"
        "If you're in development:\n"
        "  1. Build the UI: pycharter ui build\n"
        "  2. Or run in dev mode: pycharter ui dev\n\n"
        "Expected locations:\n"
        "  - Installed package: <site-packages>/ui/static/\n"
        "  - Source: ui/static/ or ui/out/"
    )


# Cache raw index.html to avoid repeated disk I/O
_index_html_cache: str | None = None
_index_html_static_dir: Path | None = None


def _get_index_html_with_config(static_dir: Path, theme: str, app_name: str) -> str:
    """Return index.html content with theme and app name script injected before </head>. Caches raw file."""
    global _index_html_cache, _index_html_static_dir
    if _index_html_cache is None or _index_html_static_dir != static_dir:
        index_path = static_dir / "index.html"
        _index_html_cache = index_path.read_text(encoding="utf-8", errors="replace")
        _index_html_static_dir = static_dir
    content = _index_html_cache
    if "</head>" not in content:
        return content
    theme_js = json.dumps(theme)
    app_name_js = json.dumps(app_name)
    # Named palettes (data-theme); light/dark/system use :root + .dark only. Must match NAMED_THEME_IDS in UI.
    named_themes_js = (
        '["ocean","forest","sunset","high-contrast","sepia","violet","cathay"]'
    )
    script = (
        f'<script>window.__APP_CONFIG__={{"theme":{theme_js},"appName":{app_name_js}}};'
        'var theme=window.__APP_CONFIG__.theme||"light";'
        f"var namedThemes={named_themes_js};"
        'function resolve(t){if(t==="system")return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";return t;}'
        "function apply(){var r=resolve(theme);var el=document.documentElement;"
        'if(r==="dark"){el.classList.add("dark");el.removeAttribute("data-theme");}'
        'else{el.classList.remove("dark");'
        'if(namedThemes.indexOf(r)!==-1)el.setAttribute("data-theme",r);else el.removeAttribute("data-theme");}'
        "}apply();"
        'if(theme==="system")window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(){apply();});'
        "</script></head>"
    )
    return content.replace("</head>", script, 1)


def create_app(api_url: str) -> FastAPI:
    """Create FastAPI app for serving UI and proxying API requests."""
    app = FastAPI(title="PyCharter UI")

    static_dir = get_static_dir()

    # Mount _next static files (must be before catch-all route)
    # Next.js exports _next directory at the root of the static output
    _next_dir = static_dir / "_next"
    if _next_dir.exists():
        # Use absolute path to avoid any path resolution issues
        _next_dir_abs = _next_dir.resolve()
        app.mount(
            "/_next", StaticFiles(directory=str(_next_dir_abs), html=False), name="next"
        )

    # Mount other static assets (images, fonts, etc.) if they exist
    static_assets_dir = static_dir / "static"
    if static_assets_dir.exists() and static_assets_dir != _next_dir:
        app.mount(
            "/static",
            StaticFiles(directory=str(static_assets_dir), html=False),
            name="static",
        )

    # Proxy API requests (must be before catch-all route)
    @app.api_route(
        "/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
    )
    async def proxy_api(request: Request, path: str):
        """Proxy API requests to the PyCharter API."""
        url = f"{api_url.rstrip('/')}/api/{path}"

        # Forward query parameters
        if request.url.query:
            url = f"{url}?{request.url.query}"

        # Get request body if present
        body = None
        if request.method in ["POST", "PUT", "PATCH"]:
            body = await request.body()

        # Forward headers (excluding host and connection)
        headers = dict(request.headers)
        headers.pop("host", None)
        headers.pop("connection", None)
        headers.pop("content-length", None)

        async with httpx.AsyncClient() as client:
            try:
                response = await client.request(
                    method=request.method,
                    url=url,
                    headers=headers,
                    content=body,
                    timeout=30.0,
                )

                return Response(
                    content=response.content,
                    status_code=response.status_code,
                    headers=dict(response.headers),
                    media_type=response.headers.get("content-type"),
                )
            except httpx.RequestError as e:
                return Response(
                    content=f"API request failed: {str(e)}",
                    status_code=502,
                    media_type="text/plain",
                )

    # Serve index.html for all other routes (SPA routing)
    # This catch-all must come last and should not match /api or /_next
    # Allow HEAD so prefetch/link-checkers get 200 instead of 405
    @app.api_route("/{path:path}", methods=["GET", "HEAD"])
    async def serve_spa(request: Request, path: str):
        """Serve the SPA index.html for all non-API, non-static routes."""
        # These routes should already be handled by mounts/API proxy above
        # But just in case, return 404 for them
        if (
            path.startswith("api/")
            or path.startswith("_next/")
            or path.startswith("static/")
        ):
            return Response(content="Not found", status_code=404)

        # Try to serve the file if it exists (for other static assets)
        file_path_obj = static_dir / path
        if file_path_obj.exists() and file_path_obj.is_file():
            if request.method == "HEAD":
                return Response(status_code=200)
            return FileResponse(str(file_path_obj))

        # Fallback to index.html for SPA routing
        index_file = static_dir / "index.html"
        if not index_file.exists():
            return Response(
                content="UI not built. Please run 'pycharter ui build' first.",
                status_code=404,
                media_type="text/plain",
            )
        if request.method == "HEAD":
            return Response(status_code=200, media_type="text/html")
        from pycharter.config import get_ui_app_name, get_ui_theme

        html = _get_index_html_with_config(
            static_dir, get_ui_theme(), get_ui_app_name()
        )
        return Response(content=html, media_type="text/html")

    return app


def serve_ui(
    api_url: str | None = None,
    host: str = "127.0.0.1",
    port: int = 3002,
) -> None:
    """
    Serve the PyCharter UI.

    Args:
        api_url: URL of the PyCharter API (defaults to http://localhost:8002)
        host: Host to bind to (default: 127.0.0.1)
        port: Port to bind to (default: 3002)
    """
    # Get API URL from environment or argument
    if api_url is None:
        api_url = os.getenv("PYCHARTER_API_URL", "http://localhost:8002")

    # Validate that static files exist
    try:
        static_dir = get_static_dir()
        print(f"✓ Found UI static files at: {static_dir}")
    except FileNotFoundError as e:
        print(f"❌ Error: {e}", file=sys.stderr)
        sys.exit(1)

    # Create and run the app
    app = create_app(api_url)

    print(f"PyCharter UI server starting...")
    print(f"  UI: http://{host}:{port}")
    print(f"  API: {api_url}")

    uvicorn.run(app, host=host, port=port, log_level="info")


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="PyCharter UI Server")
    parser.add_argument(
        "--api-url",
        type=str,
        default=None,
        help="URL of the PyCharter API (default: http://localhost:8002 or PYCHARTER_API_URL env var)",
    )
    parser.add_argument(
        "--host",
        type=str,
        default="127.0.0.1",
        help="Host to bind to (default: 127.0.0.1)",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=3002,
        help="Port to bind to (default: 3002)",
    )

    args = parser.parse_args()
    serve_ui(api_url=args.api_url, host=args.host, port=args.port)
