"""
Temporary localhost HTTP server for receiving OAuth redirects.

Starts on a random available port, listens for exactly ONE GET request
carrying ``?code=...&state=...``, returns a "Success!" HTML page, and
shuts down.  Same pattern used by Claude Code, VS Code, and GitHub CLI.

The server runs in an asyncio task and yields the received auth code
and state through a future. A configurable timeout (default 300s / 5 minutes)
prevents the server from hanging indefinitely if the user abandons
the flow.
"""

from __future__ import annotations

import asyncio
import logging
import socket
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from typing import Optional
from urllib.parse import parse_qs, urlparse

log = logging.getLogger("brynq.runtime.auth.callback_server")

# ── Data Classes ─────────────────────────────────────────────────────────


@dataclass(frozen=True)
class CallbackResult:
    """Data received from the OAuth redirect."""

    code: str
    state: str


# ── Success Page HTML ────────────────────────────────────────────────────

_SUCCESS_HTML = """\
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Brynq - Authorization Complete</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            background: #0a0a0a;
            color: #e0e0e0;
        }
        .card {
            text-align: center;
            padding: 3rem;
            border: 1px solid #333;
            border-radius: 12px;
            background: #141414;
            max-width: 440px;
        }
        .checkmark {
            font-size: 3rem;
            margin-bottom: 1rem;
        }
        h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
        p { color: #888; margin: 0; line-height: 1.5; }
    </style>
</head>
<body>
    <div class="card">
        <div class="checkmark">&#10003;</div>
        <h1>Authorization Complete</h1>
        <p>You can close this tab and return to Brynq.</p>
    </div>
</body>
</html>
"""

_ERROR_HTML = """\
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Brynq - Authorization Failed</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            background: #0a0a0a;
            color: #e0e0e0;
        }
        .card {
            text-align: center;
            padding: 3rem;
            border: 1px solid #333;
            border-radius: 12px;
            background: #141414;
            max-width: 440px;
        }
        .xmark { font-size: 3rem; margin-bottom: 1rem; color: #e74c3c; }
        h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
        p { color: #888; margin: 0; line-height: 1.5; }
    </style>
</head>
<body>
    <div class="card">
        <div class="xmark">&#10007;</div>
        <h1>Authorization Failed</h1>
        <p>{{ERROR_MESSAGE}}</p>
        <p style="margin-top:1rem;">Please close this tab and try again in Brynq.</p>
    </div>
</body>
</html>
"""


# ── Callback Handler ─────────────────────────────────────────────────────


def _make_handler(result_future: asyncio.Future, loop: asyncio.AbstractEventLoop):
    """Create a request handler class that writes to the given future."""

    class _CallbackHandler(BaseHTTPRequestHandler):
        def do_GET(self) -> None:  # noqa: N802 — required by BaseHTTPRequestHandler
            parsed = urlparse(self.path)
            log.debug(
                "Request: %s %s (query length: %d)",
                self.command,
                parsed.path,
                len(parsed.query),
            )
            params = parse_qs(parsed.query)

            # Check for OAuth error response
            error = params.get("error", [None])[0]
            if error:
                error_desc = params.get("error_description", [error])[0]
                self._send_error_page(error_desc)
                loop.call_soon_threadsafe(
                    result_future.set_exception,
                    OAuthCallbackError(f"Provider returned error: {error_desc}"),
                )
                return

            code_list = params.get("code", [])
            state_list = params.get("state", [])

            if not code_list:
                self._send_error_page("Missing authorization code.")
                loop.call_soon_threadsafe(
                    result_future.set_exception,
                    OAuthCallbackError("Missing 'code' parameter in callback"),
                )
                return

            code = code_list[0]
            state = state_list[0] if state_list else ""

            # Send success page
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(_SUCCESS_HTML.encode("utf-8"))

            # Deliver result to the waiting coroutine
            result = CallbackResult(code=code, state=state)
            loop.call_soon_threadsafe(result_future.set_result, result)

        def _send_error_page(self, message: str) -> None:
            import html as html_mod
            html = _ERROR_HTML.replace("{{ERROR_MESSAGE}}", html_mod.escape(message))
            self.send_response(400)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(html.encode("utf-8"))

        def log_message(self, format: str, *args: object) -> None:
            """Suppress default stderr logging from BaseHTTPRequestHandler."""
            log.debug("Callback server: %s", format % args)

    return _CallbackHandler


# ── Exceptions ───────────────────────────────────────────────────────────


class OAuthCallbackError(Exception):
    """Raised when the OAuth callback fails."""


class OAuthCallbackTimeout(OAuthCallbackError):
    """Raised when the callback server times out waiting for a redirect."""


# ── Callback Server ─────────────────────────────────────────────────────


class OAuthCallbackServer:
    """Temporary localhost HTTP server that receives a single OAuth redirect.

    Usage::

        server = OAuthCallbackServer()
        port = server.port              # random available port
        redirect_uri = server.redirect_uri  # http://localhost:{port}/callback

        # ... send user to provider with redirect_uri ...

        result = await server.wait_for_callback(timeout=300)
        print(result.code, result.state)

    The server binds to a random port on instantiation and starts serving
    when ``wait_for_callback()`` is called. It shuts down after receiving
    exactly one request (or on timeout).
    """

    def __init__(self, host: str = "127.0.0.1", port: int = 0) -> None:
        """Initialize the callback server.

        Args:
            host: Bind address (default localhost only).
            port: Port to bind on (0 = random available port).
        """
        self._host = host
        # Find an available port
        if port == 0:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.bind((host, 0))
            self._port = sock.getsockname()[1]
            sock.close()
        else:
            self._port = port

        self._server: Optional[HTTPServer] = None
        self._thread: Optional[Thread] = None

    @property
    def port(self) -> int:
        """The port this server will listen on."""
        return self._port

    @property
    def redirect_uri(self) -> str:
        """The full redirect URI to register with the OAuth provider."""
        return f"http://{self._host}:{self._port}/callback"

    async def wait_for_callback(self, timeout: float = 300.0) -> CallbackResult:
        """Start the server and wait for the OAuth redirect.

        Args:
            timeout: Maximum seconds to wait before giving up.

        Returns:
            CallbackResult with the authorization code and state.

        Raises:
            OAuthCallbackTimeout: If no callback is received within timeout.
            OAuthCallbackError: If the callback contains an error.
        """
        loop = asyncio.get_running_loop()
        result_future: asyncio.Future[CallbackResult] = loop.create_future()

        handler_class = _make_handler(result_future, loop)
        self._server = HTTPServer((self._host, self._port), handler_class)
        self._server.timeout = timeout  # unblock handle_request() on timeout
        # Update port in case the OS assigned a different one
        self._port = self._server.server_address[1]

        # Run the HTTP server in a background thread so we don't block the
        # event loop. The server handles exactly one request then we shut down.
        self._thread = Thread(
            target=self._serve_one,
            daemon=True,
            name="oauth-callback-server",
        )
        self._thread.start()
        log.info(
            "OAuth callback server listening on %s:%d", self._host, self._port
        )

        try:
            result = await asyncio.wait_for(result_future, timeout=timeout)
        except asyncio.TimeoutError:
            raise OAuthCallbackTimeout(
                f"No OAuth callback received within {timeout}s. "
                "Did you complete the login in your browser?"
            )
        finally:
            self._shutdown()

        return result

    def _serve_one(self) -> None:
        """Handle exactly one request, then stop."""
        if self._server:
            self._server.handle_request()

    def _shutdown(self) -> None:
        """Shut down the HTTP server and join the thread."""
        if self._server:
            try:
                self._server.server_close()
            except Exception:
                pass
            self._server = None
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=5)
            self._thread = None
        log.debug("OAuth callback server shut down")
