"""OAuth2 PKCE flow — parameterized port of yt-mcp's oauthFlow.ts."""

from __future__ import annotations

import asyncio
import hashlib
import html
import secrets
import socket
import webbrowser
from base64 import urlsafe_b64encode
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from urllib.parse import parse_qs, urlencode, urlparse

import httpx


def _b64url(data: bytes) -> str:
    """Encode bytes as URL-safe base64 without padding."""
    return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")


async def run_oauth_flow(
    auth_url: str,
    *,
    client_name: str,
    timeout_seconds: int = 300,
) -> dict:
    """Run full OAuth2 PKCE flow, returning tokens dict.

    Args:
        auth_url: Auth gateway base URL
        client_name: DCR client name (e.g., "reddit-mcp-cli")
        timeout_seconds: Timeout for waiting on user authorization (default: 300)

    Returns:
        {"access_token", "refresh_token", "expires_in", "client_id"}
    """
    # 1. Generate PKCE + state
    code_verifier = _b64url(secrets.token_bytes(32))
    code_challenge = _b64url(hashlib.sha256(code_verifier.encode("ascii")).digest())
    state = _b64url(secrets.token_bytes(32))

    # 2. Start temp HTTP server on random port
    port = _find_free_port()
    redirect_uri = f"http://127.0.0.1:{port}"

    # 3. DCR register client
    async with httpx.AsyncClient() as client:
        dcr_resp = await client.post(
            f"{auth_url}/oauth/register",
            json={
                "client_name": client_name,
                "redirect_uris": [redirect_uri],
            },
        )
        if dcr_resp.status_code not in (200, 201):
            raise RuntimeError(
                f"DCR registration failed: {dcr_resp.status_code} {dcr_resp.text}"
            )
        dcr_body = dcr_resp.json()
        client_id: str = dcr_body["client_id"]
        client_secret: str | None = dcr_body.get("client_secret")

    # 4. Start callback server + open browser
    result_future: asyncio.Future[dict] = asyncio.get_event_loop().create_future()
    server = _start_callback_server(
        port=port,
        state=state,
        auth_url=auth_url,
        redirect_uri=redirect_uri,
        client_id=client_id,
        client_secret=client_secret,
        code_verifier=code_verifier,
        result_future=result_future,
    )
    server_thread = Thread(target=server.serve_forever, daemon=True)
    server_thread.start()

    authorize_url = (
        f"{auth_url}/oauth/authorize?"
        + urlencode(
            {
                "response_type": "code",
                "client_id": client_id,
                "redirect_uri": redirect_uri,
                "code_challenge": code_challenge,
                "code_challenge_method": "S256",
                "state": state,
            }
        )
    )

    print(f"\n\033[1mOpen this URL in your browser to authorize:\033[0m")
    print(f"\n  {authorize_url}\n")

    try:
        webbrowser.open(authorize_url)
    except Exception:
        pass  # user can open manually

    # 5. Wait for callback
    try:
        tokens = await asyncio.wait_for(result_future, timeout=timeout_seconds)
    except asyncio.TimeoutError:
        raise RuntimeError(f"OAuth flow timed out after {timeout_seconds} seconds")
    finally:
        server.shutdown()

    return {**tokens, "client_id": client_id}


# -- internal helpers -----------------------------------------------------------


def _find_free_port() -> int:
    """Find a free port on 127.0.0.1."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def _start_callback_server(
    *,
    port: int,
    state: str,
    auth_url: str,
    redirect_uri: str,
    client_id: str,
    client_secret: str | None,
    code_verifier: str,
    result_future: asyncio.Future[dict],
) -> HTTPServer:
    loop = asyncio.get_event_loop()

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self) -> None:  # noqa: N802
            parsed = urlparse(self.path)
            params = parse_qs(parsed.query)

            error = params.get("error", [None])[0]
            if error:
                safe_error = html.escape(str(error))
                self._html(f"<h1>Authorization failed</h1><p>{safe_error}</p>")
                loop.call_soon_threadsafe(
                    result_future.set_exception,
                    RuntimeError(f"OAuth error: {error}"),
                )
                return

            code = params.get("code", [None])[0]
            if not code:
                self._html("<h1>Waiting for authorization...</h1>")
                return

            returned_state = params.get("state", [None])[0]
            if returned_state != state:
                self._html(
                    "<h1>Authorization failed</h1><p>State mismatch — possible CSRF.</p>"
                )
                loop.call_soon_threadsafe(
                    result_future.set_exception,
                    RuntimeError("OAuth state mismatch"),
                )
                return

            # Exchange code for tokens (sync in handler thread)
            token_body: dict[str, str] = {
                "grant_type": "authorization_code",
                "code": code,
                "redirect_uri": redirect_uri,
                "client_id": client_id,
                "code_verifier": code_verifier,
            }
            if client_secret:
                token_body["client_secret"] = client_secret

            try:
                with httpx.Client() as http:
                    resp = http.post(f"{auth_url}/oauth/token", json=token_body)
                if resp.status_code != 200:
                    safe_text = html.escape(resp.text[:200])
                    self._html(
                        f"<h1>Token exchange failed</h1><p>{safe_text}</p>"
                    )
                    loop.call_soon_threadsafe(
                        result_future.set_exception,
                        RuntimeError(f"Token exchange failed: {resp.status_code}"),
                    )
                    return

                tokens = resp.json()
                self._html(
                    "<h1>Authorization successful!</h1><p>You can close this window.</p>"
                )
                loop.call_soon_threadsafe(result_future.set_result, tokens)

            except Exception as exc:
                self._html("<h1>Internal error</h1>")
                loop.call_soon_threadsafe(result_future.set_exception, exc)

        def _html(self, body: str) -> None:
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(body.encode("utf-8"))

        def log_message(self, format: str, *args: object) -> None:  # noqa: A002
            pass  # suppress request logs

    server = HTTPServer(("127.0.0.1", port), Handler)
    return server
