from __future__ import annotations

import socket
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from urllib.parse import parse_qs, urlparse


def _callback_parse_error_result(e: Exception) -> dict[str, str]:
    msg = str(e).strip() or e.__class__.__name__
    return {"code": "", "state": "", "error": f"failed to parse oauth callback: {msg}"}


def _parse_callback_result(path: str) -> dict[str, str]:
    qs = parse_qs(urlparse(path).query)
    code = (qs.get("code") or [""])[0]
    state = (qs.get("state") or [""])[0]
    error = (qs.get("error") or [""])[0]
    return {"code": code, "state": state, "error": error}


def _is_meaningful_callback_result(result: dict[str, str]) -> bool:
    return bool(result.get("code") or result.get("state") or result.get("error"))


class OAuthCallbackServer:
    """Minimal localhost callback server for OAuth auth-code flow (PKCE).

    This matches the upstream behavior: start a temporary HTTP server bound to the
    redirect port and wait for a single callback containing `code` + `state`.
    """

    def __init__(self, *, host: str = "localhost", port: int):
        self._host = str(host or "localhost")
        self._port = int(port)
        self._server: HTTPServer | None = None
        self._thread: threading.Thread | None = None
        self._event = threading.Event()
        self._result: dict[str, Any] = {}

    @property
    def result(self) -> dict[str, Any]:
        return dict(self._result)

    def start(self) -> None:
        parent = self

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:  # noqa: N802
                if not parent._event.is_set():
                    try:
                        parsed = _parse_callback_result(self.path)
                    except Exception as e:
                        parent._result = _callback_parse_error_result(e)
                        parent._event.set()
                    else:
                        if _is_meaningful_callback_result(parsed):
                            parent._result = parsed
                            parent._event.set()
                try:
                    self.send_response(200)
                    self.send_header("Content-Type", "text/plain; charset=utf-8")
                    self.end_headers()
                    self.wfile.write(b"OAuth callback received. You can close this tab.")
                except Exception:
                    # The callback result is already captured above; ignore broken
                    # client connections while writing the browser response.
                    return

            def log_message(self, format: str, *args: Any) -> None:  # noqa: A002, ANN401
                # Keep CLI output clean.
                _ = format, args
                return

        server_cls = HTTPServer
        if ":" in self._host:
            class _IPv6HTTPServer(HTTPServer):
                address_family = socket.AF_INET6

            server_cls = _IPv6HTTPServer
        self._server = server_cls((self._host, self._port), Handler)
        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
        self._thread.start()

    def wait_for_callback(self, *, timeout_s: float = 300.0) -> dict[str, Any]:
        ok = self._event.wait(timeout=float(timeout_s))
        if not ok:
            raise TimeoutError("Timed out waiting for OAuth callback.")
        return dict(self._result)

    def close(self) -> None:
        if self._server is not None:
            try:
                self._server.shutdown()
            except Exception:
                pass
            try:
                self._server.server_close()
            except Exception:
                pass
        self._server = None

