"""gRPC interceptors for authentication.

This module provides both sync and async interceptors for adding
authentication metadata to gRPC calls. All interceptors read the
token from a shared TokenHolder instance, enabling runtime token
rotation without reconnection.
"""

from __future__ import annotations

from collections.abc import AsyncIterable, Callable
from typing import TYPE_CHECKING, Any

import grpc
import grpc.aio
from grpc import ClientCallDetails

if TYPE_CHECKING:
    from kubemq._internal.auth import TokenHolder


_PING_METHOD_SUFFIX = "/Ping"
_PING_METHOD_SUFFIX_BYTES = b"/Ping"


def _is_ping_method(method: str | bytes | None) -> bool:
    """Return True if the RPC method is the Ping health-check (bypasses auth)."""
    if not method:
        return False
    if isinstance(method, bytes):
        return method.endswith(_PING_METHOD_SUFFIX_BYTES)
    return method.endswith(_PING_METHOD_SUFFIX)


def _inject_auth_metadata(
    metadata: tuple[tuple[str, str], ...] | None,
    token_holder: TokenHolder,
    *,
    method: str | None = None,
) -> tuple[tuple[str, str], ...]:
    """Inject auth token from TokenHolder into gRPC metadata.

    Args:
        metadata: Existing metadata tuple or None.
        token_holder: Shared mutable token container.
        method: RPC method path. Ping bypasses authentication.

    Returns:
        New metadata tuple with auth token added (if present).
    """
    if _is_ping_method(method):
        return metadata or ()

    token = token_holder.token
    if not token or not token.strip():
        return metadata or ()

    existing = list(metadata or [])
    existing.append(("authorization", token))
    return tuple(existing)


# =============================================================================
# Sync Interceptors
# =============================================================================


class AuthInterceptors(
    grpc.UnaryUnaryClientInterceptor,  # type: ignore[misc]
    grpc.StreamUnaryClientInterceptor,  # type: ignore[misc]
    grpc.UnaryStreamClientInterceptor,  # type: ignore[misc]
    grpc.StreamStreamClientInterceptor,  # type: ignore[misc]
):
    """Sync interceptor that adds auth token to all gRPC calls."""

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    def _intercept_call(
        self,
        continuation: Callable[[ClientCallDetails, Any], Any],
        client_call_details: ClientCallDetails,
        request_or_iterator: Any,
    ) -> Any:
        method = getattr(client_call_details, "method", None)
        if _is_ping_method(method):
            return continuation(client_call_details, request_or_iterator)

        metadata = list(client_call_details.metadata or [])

        token = self._token_holder.token
        if token and token.strip():
            metadata.append(("authorization", token))

        new_client_call_details = client_call_details._replace(metadata=metadata)
        response = continuation(new_client_call_details, request_or_iterator)
        return response

    def intercept_unary_unary(
        self, continuation: Any, client_call_details: Any, request: Any
    ) -> Any:
        """Intercept unary-unary calls to inject auth metadata."""
        return self._intercept_call(continuation, client_call_details, request)

    def intercept_unary_stream(
        self, continuation: Any, client_call_details: Any, request: Any
    ) -> Any:
        """Intercept unary-stream calls to inject auth metadata."""
        return self._intercept_call(continuation, client_call_details, request)

    def intercept_stream_stream(
        self, continuation: Any, client_call_details: Any, request_iterator: Any
    ) -> Any:
        """Intercept stream-stream calls to inject auth metadata."""
        return self._intercept_call(continuation, client_call_details, request_iterator)

    def intercept_stream_unary(
        self, continuation: Any, client_call_details: Any, request_iterator: Any
    ) -> Any:
        """Intercept stream-unary calls to inject auth metadata."""
        return self._intercept_call(continuation, client_call_details, request_iterator)


# =============================================================================
# Async Interceptors - Combined class (legacy support)
# =============================================================================


class AuthInterceptorsAsync(
    grpc.aio.UnaryUnaryClientInterceptor,  # type: ignore[misc]
    grpc.aio.StreamStreamClientInterceptor,  # type: ignore[misc]
    grpc.aio.UnaryStreamClientInterceptor,  # type: ignore[misc]
    grpc.aio.StreamUnaryClientInterceptor,  # type: ignore[misc]
):
    """Async interceptor that adds auth token to all gRPC calls.

    Note: This combined class is kept for backward compatibility.
    For Phase 4 native async, use the separate interceptor classes below.
    """

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    async def _intercept_call(
        self,
        continuation: Callable[[ClientCallDetails, Any], Any],
        client_call_details: ClientCallDetails,
        request_or_iterator: Any,
    ) -> Any:
        method = getattr(client_call_details, "method", None)
        if _is_ping_method(method):
            return await continuation(client_call_details, request_or_iterator)

        metadata = list(client_call_details.metadata or [])

        token = self._token_holder.token
        if token and token.strip():
            metadata.append(("authorization", token))

        new_client_call_details = client_call_details._replace(metadata=metadata)
        response = await continuation(new_client_call_details, request_or_iterator)
        return response

    async def intercept_unary_unary(
        self, continuation: Any, client_call_details: Any, request: Any
    ) -> Any:
        """Intercept async unary-unary calls to inject auth metadata."""
        return await self._intercept_call(continuation, client_call_details, request)

    async def intercept_unary_stream(
        self, continuation: Any, client_call_details: Any, request: Any
    ) -> Any:
        """Intercept async unary-stream calls to inject auth metadata."""
        return await self._intercept_call(continuation, client_call_details, request)

    async def intercept_stream_stream(
        self, continuation: Any, client_call_details: Any, request_iterator: Any
    ) -> Any:
        """Intercept async stream-stream calls to inject auth metadata."""
        return await self._intercept_call(continuation, client_call_details, request_iterator)

    async def intercept_stream_unary(
        self, continuation: Any, client_call_details: Any, request_iterator: Any
    ) -> Any:
        """Intercept async stream-unary calls to inject auth metadata."""
        return await self._intercept_call(continuation, client_call_details, request_iterator)


# =============================================================================
# Async Interceptors - Separate classes (Phase 4 native async)
# =============================================================================


class AsyncUnaryUnaryAuthInterceptor(grpc.aio.UnaryUnaryClientInterceptor):  # type: ignore[misc]
    """Async interceptor for unary-unary calls (ping, send_event, etc.).

    This interceptor is used for simple request-response operations like
    Ping, SendEvent, SendRequest, SendResponse, etc.
    """

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    async def intercept_unary_unary(
        self,
        continuation: Callable[..., Any],
        client_call_details: grpc.aio.ClientCallDetails,
        request: Any,
    ) -> Any:
        """Intercept unary-unary calls to inject auth metadata."""
        new_details = grpc.aio.ClientCallDetails(
            method=client_call_details.method,
            timeout=client_call_details.timeout,
            metadata=_inject_auth_metadata(
                client_call_details.metadata, self._token_holder, method=client_call_details.method
            ),
            credentials=client_call_details.credentials,
            wait_for_ready=client_call_details.wait_for_ready,
        )
        return await continuation(new_details, request)


class AsyncUnaryStreamAuthInterceptor(grpc.aio.UnaryStreamClientInterceptor):  # type: ignore[misc]
    """Async interceptor for unary-stream calls (subscriptions).

    This interceptor is CRITICAL for SubscribeToEvents, SubscribeToRequests, etc.
    """

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    async def intercept_unary_stream(
        self,
        continuation: Callable[..., Any],
        client_call_details: grpc.aio.ClientCallDetails,
        request: Any,
    ) -> grpc.aio.UnaryStreamCall:
        """Intercept unary-stream calls to inject auth metadata."""
        new_details = grpc.aio.ClientCallDetails(
            method=client_call_details.method,
            timeout=client_call_details.timeout,
            metadata=_inject_auth_metadata(
                client_call_details.metadata, self._token_holder, method=client_call_details.method
            ),
            credentials=client_call_details.credentials,
            wait_for_ready=client_call_details.wait_for_ready,
        )
        return await continuation(new_details, request)


class AsyncStreamUnaryAuthInterceptor(grpc.aio.StreamUnaryClientInterceptor):  # type: ignore[misc]
    """Async interceptor for stream-unary calls (queue send batch)."""

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    async def intercept_stream_unary(
        self,
        continuation: Callable[..., Any],
        client_call_details: grpc.aio.ClientCallDetails,
        request_iterator: AsyncIterable[Any],
    ) -> Any:
        """Intercept stream-unary calls to inject auth metadata."""
        new_details = grpc.aio.ClientCallDetails(
            method=client_call_details.method,
            timeout=client_call_details.timeout,
            metadata=_inject_auth_metadata(
                client_call_details.metadata, self._token_holder, method=client_call_details.method
            ),
            credentials=client_call_details.credentials,
            wait_for_ready=client_call_details.wait_for_ready,
        )
        return await continuation(new_details, request_iterator)


class AsyncStreamStreamAuthInterceptor(grpc.aio.StreamStreamClientInterceptor):  # type: ignore[misc]
    """Async interceptor for bidirectional streaming (queue upstream/downstream).

    This interceptor is CRITICAL for QueuesUpstream and QueuesDownstream.
    """

    def __init__(self, token_holder: TokenHolder) -> None:
        self._token_holder = token_holder

    async def intercept_stream_stream(
        self,
        continuation: Callable[..., Any],
        client_call_details: grpc.aio.ClientCallDetails,
        request_iterator: AsyncIterable[Any],
    ) -> grpc.aio.StreamStreamCall:
        """Intercept stream-stream calls to inject auth metadata."""
        new_details = grpc.aio.ClientCallDetails(
            method=client_call_details.method,
            timeout=client_call_details.timeout,
            metadata=_inject_auth_metadata(
                client_call_details.metadata, self._token_holder, method=client_call_details.method
            ),
            credentials=client_call_details.credentials,
            wait_for_ready=client_call_details.wait_for_ready,
        )
        return await continuation(new_details, request_iterator)
