"""Local HTTP server for the Orxaq dashboard.

Serves static HTML and API routes using http.server (stdlib).
Supports skin selection (gamma, beta, alpha) via --skin flag.
Auth middleware optional via ORXAQ_AUTH_ENABLED=true.
Stdlib-only (ADR-022).
"""

from __future__ import annotations

import json
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import TYPE_CHECKING

from orxaq.experiences.api import OrxaqAPI
from orxaq.experiences.dag_editor.export import DAGExporter
from orxaq.experiences.dag_editor.proposals import EdgeProposer
from orxaq.experiences.dag_editor.scoring import SolitonScorer
from orxaq.experiences.editor import DAGEditorAPI
from orxaq.experiences.websocket import WebSocketHub, handle_websocket_upgrade, is_websocket_upgrade

if TYPE_CHECKING:
    from orxaq.enterprise.auth import AuthMiddleware

STATIC_DIR = Path(__file__).parent / "static"


class OrxaqHandler(BaseHTTPRequestHandler):
    """HTTP request handler for Orxaq dashboard."""

    api: OrxaqAPI
    editor: DAGEditorAPI
    ws_hub: WebSocketHub | None = None
    auth: AuthMiddleware | None = None
    skin_name: str = "gamma"
    _skin_html_cache: dict[str, str] = {}

    def _check_auth(self, path: str) -> bool:
        """Check authentication if enabled. Returns True if request is allowed."""
        middleware = self.__class__.auth
        if middleware is None:
            return True
        # Normalize header keys for case-insensitive auth lookup.
        headers: dict[str, str] = {}
        for k, v in self.headers.items():
            lower_k = k.lower()
            if lower_k == "x-api-key":
                headers["X-API-Key"] = v
            elif lower_k == "authorization":
                headers["Authorization"] = v
            else:
                headers[k] = v
        result = middleware.authenticate(headers, path)
        if result.rate_limited:
            self._send_json({"error": "rate_limited"}, 429)
            return False
        if not result.authenticated:
            self._send_json({"error": result.error or "unauthorized"}, 401)
            return False
        return True

    def do_GET(self) -> None:  # noqa: N802
        """Handle GET requests."""
        path = self.path.split("?")[0]

        # WebSocket upgrade for /ws/events
        if path == "/ws/events" and is_websocket_upgrade(self.headers):
            if self.__class__.ws_hub is not None:
                handle_websocket_upgrade(self, self.__class__.ws_hub)
            else:
                self._send_json({"error": "websocket_not_configured"}, 503)
            return

        # Auth check for API routes
        if path.startswith("/api/") and not self._check_auth(path):
            return

        # DAG export format routes
        if path.startswith("/api/dag/export/"):
            fmt = path.rsplit("/", 1)[-1]
            self._handle_export(fmt)
            return

        # DAG editor routes
        if path.startswith("/api/dag/"):
            response, status = self.editor.handle(path, "GET")
            self._send_json(response, status)
            return

        # WebSocket status endpoint
        if path == "/api/ws/status":
            hub = self.__class__.ws_hub
            self._send_json(
                {
                    "enabled": hub is not None,
                    "clients": hub.client_count() if hub else 0,
                }
            )
            return

        # API routes
        if path.startswith("/api/"):
            self._handle_api(path)
            return

        # Editor page
        if path == "/editor":
            self._serve_file("editor.html", "text/html")
            return

        # Dashboard root
        if path == "/" or path == "/index.html":
            self._serve_dashboard()
            return

        self._send_json({"error": "not_found"}, 404)

    def do_POST(self) -> None:  # noqa: N802
        """Handle POST requests."""
        path = self.path.split("?")[0]

        if path.startswith("/api/") and not self._check_auth(path):
            return

        body = self._read_body()

        if path == "/api/dag/propose-edges":
            self._handle_propose(body)
            return

        if path == "/api/dag/score":
            self._handle_score()
            return

        if path.startswith("/api/dag/"):
            response, status = self.editor.handle(path, "POST", body)
            self._send_json(response, status)
            return

        self._send_json({"error": "not_found"}, 404)

    def do_DELETE(self) -> None:  # noqa: N802
        """Handle DELETE requests."""
        path = self.path.split("?")[0]

        if path.startswith("/api/") and not self._check_auth(path):
            return

        body = self._read_body()

        if path.startswith("/api/dag/"):
            response, status = self.editor.handle(path, "DELETE", body)
            self._send_json(response, status)
            return

        self._send_json({"error": "not_found"}, 404)

    def do_OPTIONS(self) -> None:  # noqa: N802
        """Handle CORS preflight."""
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
        self.end_headers()

    def _read_body(self) -> str:
        """Read request body."""
        length = int(self.headers.get("Content-Length", 0))
        if length == 0:
            return ""
        return self.rfile.read(length).decode("utf-8")

    def _handle_propose(self, body: str) -> None:
        """Handle AI edge proposals."""
        dag = self.editor.engine.world_dag
        proposer = EdgeProposer(dag)
        node_id = None
        if body:
            try:
                data = json.loads(body)
                node_id = data.get("node")
            except (ValueError, AttributeError):
                pass
        if node_id:
            proposals = proposer.propose_for_node(node_id)
        else:
            proposals = proposer.propose_all()
        self._send_json({"proposals": proposals}, 200)

    def _handle_score(self) -> None:
        """Handle soliton score request."""
        dag = self.editor.engine.world_dag
        scorer = SolitonScorer(dag)
        self._send_json(
            {
                "overall": scorer.overall_score(),
                "components": scorer.component_scores(),
                "node_scores": scorer.node_scores(),
            },
            200,
        )

    def _handle_export(self, fmt: str) -> None:
        """Handle DAG export in various formats."""
        dag = self.editor.engine.world_dag
        exporter = DAGExporter(dag)
        if fmt == "json":
            self._send_text(exporter.to_json(), "application/json")
        elif fmt == "dot":
            self._send_text(exporter.to_dot(), "text/plain")
        elif fmt == "mermaid":
            self._send_text(exporter.to_mermaid(), "text/plain")
        else:
            self._send_json({"error": f"unknown format: {fmt}"}, 400)

    def _handle_api(self, path: str) -> None:
        """Dispatch API request."""
        response, status = self.api.handle(path)
        self._send_json(response, status)

    def _send_json(self, data: dict, status: int = 200) -> None:
        """Send JSON response with CORS headers."""
        body = json.dumps(data, indent=2, default=str).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _send_text(self, text: str, content_type: str) -> None:
        """Send plain text response."""
        body = text.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", f"{content_type}; charset=utf-8")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _serve_dashboard(self) -> None:
        """Serve the dashboard with the active skin."""
        skin_name = self.__class__.skin_name

        if skin_name == "gamma":
            self._serve_file("gamma.html", "text/html")
            return

        if skin_name not in self.__class__._skin_html_cache:
            from orxaq.experiences.skins.base import get_skin
            from orxaq.experiences.skins.renderer import render_skin

            skin = get_skin(skin_name)
            if skin is None:
                self._serve_file("gamma.html", "text/html")
                return
            self.__class__._skin_html_cache[skin_name] = render_skin(skin)

        html = self.__class__._skin_html_cache[skin_name]
        body = html.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _serve_file(self, filename: str, content_type: str) -> None:
        """Serve a static file."""
        filepath = STATIC_DIR / filename
        if not filepath.exists():
            self._send_json({"error": "file_not_found"}, 404)
            return
        body = filepath.read_bytes()
        self.send_response(200)
        self.send_header("Content-Type", f"{content_type}; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format: str, *args: object) -> None:  # noqa: A002
        """Suppress default logging."""


def serve(
    port: int = 8741,
    skin: str = "gamma",
    open_browser: bool = True,
    auth_enabled: bool = False,
) -> None:
    """Start the Orxaq dashboard server."""
    api = OrxaqAPI()
    editor = DAGEditorAPI(api.engine)
    ws_hub = WebSocketHub()

    OrxaqHandler.skin_name = skin
    OrxaqHandler._skin_html_cache = {}
    OrxaqHandler.ws_hub = ws_hub

    if auth_enabled:
        from orxaq.enterprise.auth import APIKeyStore, AuthMiddleware, RateLimiter, TokenProvider

        key_store = APIKeyStore()
        token_provider = TokenProvider()
        rate_limiter = RateLimiter(capacity=100, refill_rate=10.0)
        OrxaqHandler.auth = AuthMiddleware(
            key_store=key_store,
            token_provider=token_provider,
            rate_limiter=rate_limiter,
            excluded_paths=frozenset({"/", "/index.html", "/editor", "/health", "/ready"}),
        )
        raw_key, _record = key_store.generate_key(name="default", scopes=frozenset({"admin"}))
        print(f"Auth enabled. Default API key: {raw_key}")
    else:
        OrxaqHandler.auth = None

    def handler(*a, **kw):
        return _make_handler(api, editor, *a, **kw)

    server = HTTPServer(("localhost", port), handler)

    from orxaq.experiences.skins.base import get_skin

    skin_obj = get_skin(skin)
    display = skin_obj.display_name if skin_obj else skin
    print(f"Orxaq {display} running at http://localhost:{port}")
    print(f"WebSocket: ws://localhost:{port}/ws/events")
    print("Press Ctrl+C to stop.\n")

    if open_browser:
        webbrowser.open(f"http://localhost:{port}")

    try:
        server.serve_forever()
    finally:
        ws_hub.shutdown()


def _make_handler(api: OrxaqAPI, editor: DAGEditorAPI, *args, **kwargs):
    """Create handler with API and editor instances bound as class attributes."""
    OrxaqHandler.api = api
    OrxaqHandler.editor = editor
    return OrxaqHandler(*args, **kwargs)
