"""Core abort helpers for gRPC service mixins.

Provides protocol and abort functions for common gRPC error patterns.
"""


from __future__ import annotations

from collections.abc import Awaitable, Callable
from functools import wraps
from typing import NoReturn, ParamSpec, Protocol, TypeVar, cast

import grpc

from noteflow.domain.errors import DomainError

P = ParamSpec("P")
T = TypeVar("T")

# Common error messages used across mixins
ERR_CANCELLED_BY_USER = "Cancelled by user"
_ERR_UNREACHABLE = "Unreachable"


class AbortableContext(Protocol):
    """Minimal protocol for gRPC context abort operations.

    Captures just the abort method needed by error helpers,
    avoiding generic type parameters on ServicerContext.
    """

    async def abort(self, code: grpc.StatusCode, details: str) -> None:
        """Abort the RPC with given status code and details."""
        ...


async def abort_not_found(
    context: AbortableContext,
    entity_type: str,
    entity_id: str,
) -> NoReturn:
    """Abort with NOT_FOUND status.

    Consolidates the repeated "entity not found" error pattern.

    Args:
        context: gRPC servicer context.
        entity_type: Type of entity (e.g., "Meeting", "Annotation").
        entity_id: ID of the missing entity.

    Raises:
        grpc.RpcError: Always raises NOT_FOUND.
    """
    await context.abort(
        grpc.StatusCode.NOT_FOUND,
        f"{entity_type} {entity_id} not found",
    )
    # This line is unreachable but helps type checkers
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_database_required(
    context: AbortableContext,
    feature: str,
) -> NoReturn:
    """Abort with UNIMPLEMENTED for DB-only features.

    Consolidates the repeated "requires database persistence" pattern.

    Args:
        context: gRPC servicer context.
        feature: Feature that requires database (e.g., "Annotations").

    Raises:
        grpc.RpcError: Always raises UNIMPLEMENTED.
    """
    await context.abort(
        grpc.StatusCode.UNIMPLEMENTED,
        f"{feature} require database persistence",
    )
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_invalid_argument(
    context: AbortableContext,
    message: str,
) -> NoReturn:
    """Abort with INVALID_ARGUMENT status.

    Args:
        context: gRPC servicer context.
        message: Error message describing the invalid argument.

    Raises:
        grpc.RpcError: Always raises INVALID_ARGUMENT.
    """
    await context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_failed_precondition(
    context: AbortableContext,
    message: str,
) -> NoReturn:
    """Abort with FAILED_PRECONDITION status.

    Use when operation cannot proceed due to system state.

    Args:
        context: gRPC servicer context.
        message: Error message describing the precondition failure.

    Raises:
        grpc.RpcError: Always raises FAILED_PRECONDITION.
    """
    await context.abort(grpc.StatusCode.FAILED_PRECONDITION, message)
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_internal(
    context: AbortableContext,
    message: str,
) -> NoReturn:
    """Abort with INTERNAL status.

    Use for unexpected server errors.

    Args:
        context: gRPC servicer context.
        message: Error message describing the internal error.

    Raises:
        grpc.RpcError: Always raises INTERNAL.
    """
    await context.abort(grpc.StatusCode.INTERNAL, message)
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_already_exists(
    context: AbortableContext,
    message: str,
) -> NoReturn:
    """Abort with ALREADY_EXISTS status.

    Use when a resource already exists (e.g., duplicate job).

    Args:
        context: gRPC servicer context.
        message: Error message describing the conflict.

    Raises:
        grpc.RpcError: Always raises ALREADY_EXISTS.
    """
    await context.abort(grpc.StatusCode.ALREADY_EXISTS, message)
    raise AssertionError(_ERR_UNREACHABLE)


async def abort_unavailable(
    context: AbortableContext,
    message: str,
) -> NoReturn:
    """Abort with UNAVAILABLE status.

    Use when a service is temporarily unavailable.

    Args:
        context: gRPC servicer context.
        message: Error message describing the unavailability.

    Raises:
        grpc.RpcError: Always raises UNAVAILABLE.
    """
    await context.abort(grpc.StatusCode.UNAVAILABLE, message)
    raise AssertionError(_ERR_UNREACHABLE)


async def handle_domain_error(
    context: AbortableContext,
    error: DomainError,
) -> NoReturn:
    """Convert a domain error to gRPC error and abort.

    Args:
        context: gRPC servicer context.
        error: Domain error to convert.

    Raises:
        grpc.RpcError: Always raises with appropriate status.
    """
    await context.abort(error.grpc_status, error.message)
    raise AssertionError(_ERR_UNREACHABLE)


def domain_error_handler(  # noqa: UP047
    func: Callable[P, Awaitable[T]],
) -> Callable[P, Awaitable[T]]:
    """Decorator to automatically handle DomainError in gRPC methods.

    Catches DomainError exceptions and converts them to appropriate
    gRPC status codes using the error's grpc_status property.

    The decorated function must have `context` as its second positional
    argument (after `self`), which is the standard gRPC servicer signature.

    Usage:
        @domain_error_handler
        async def GetMeeting(self, request, context):
            meeting = await self._get_meeting(request.meeting_id)
            if meeting is None:
                raise NotFoundError("meeting", request.meeting_id)
            return meeting_to_proto(meeting)
    """

    @wraps(func)
    async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        try:
            return await func(*args, **kwargs)
        except DomainError as e:
            # args[0] is self, args[2] is context in gRPC methods
            # Standard signature: (self, request, context)
            context = args[2] if len(args) > 2 else kwargs.get("context")
            if context is not None:
                abortable_context = cast(AbortableContext, context)
                await abortable_context.abort(e.grpc_status, e.message)
            raise

    return wrapper
