import sys
import enum
import http
import time
import asyncio
import warnings

from types import TracebackType
from typing import Generic, Optional, Union, Type, List, Sequence, Any, cast
from typing import Dict, Tuple, TYPE_CHECKING

try:
    import ssl as _ssl
except ImportError:
    _ssl = None  # type: ignore

from h2.config import H2Configuration
from multidict import MultiDict

from .utils import Wrapper, DeadlineWrapper
from .const import Status, Cardinality
from .config import Configuration
from .stream import send_message, recv_message, StreamIterator
from .stream import _RecvType, _SendType
from .events import _DispatchChannelEvents
from .protocol import H2Protocol, AbstractHandler, Stream as _Stream, Peer
from .metadata import Deadline, USER_AGENT, decode_grpc_message, encode_timeout
from .metadata import encode_metadata, decode_metadata, _MetadataLike, _Metadata
from .metadata import _STATUS_DETAILS_KEY, decode_bin_value
from .exceptions import GRPCError, ProtocolError, StreamTerminatedError
from .encoding.base import GRPC_CONTENT_TYPE, CodecBase, StatusDetailsCodecBase
from .encoding.proto import ProtoCodec, ProtoStatusDetailsCodec
from .encoding.proto import _googleapis_available

from ._registry import channels as _channels

if TYPE_CHECKING:
    from ._typing import IReleaseStream  # noqa


_H2_OK = '200'

# https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
_H2_TO_GRPC_STATUS_MAP = {
    # 400
    str(http.HTTPStatus.BAD_REQUEST.value): Status.INTERNAL,
    # 401
    str(http.HTTPStatus.UNAUTHORIZED.value): Status.UNAUTHENTICATED,
    # 403
    str(http.HTTPStatus.FORBIDDEN.value): Status.PERMISSION_DENIED,
    # 404
    str(http.HTTPStatus.NOT_FOUND.value): Status.UNIMPLEMENTED,
    # 502
    str(http.HTTPStatus.BAD_GATEWAY.value): Status.UNAVAILABLE,
    # 503
    str(http.HTTPStatus.SERVICE_UNAVAILABLE.value): Status.UNAVAILABLE,
    # 504
    str(http.HTTPStatus.GATEWAY_TIMEOUT.value): Status.UNAVAILABLE,
    # 429
    str(http.HTTPStatus.TOO_MANY_REQUESTS.value): Status.UNAVAILABLE,
}


class Handler(AbstractHandler):
    connection_lost = False

    def accept(self, stream: Any, headers: Any, release_stream: Any) -> None:
        raise NotImplementedError('Client connection can not accept requests')

    def cancel(self, stream: Any) -> None:
        pass

    def close(self) -> None:
        self.connection_lost = True


class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
    """
    Represents gRPC method call - HTTP/2 request/stream, and everything you
    need to communicate with server in order to get response.

    In order to work directly with stream, you should
    :py:meth:`ServiceMethod.open` request like this:

    .. code-block:: python3

        request = cafe_pb2.LatteOrder(
            size=cafe_pb2.SMALL,
            temperature=70,
            sugar=3,
        )
        async with client.MakeLatte.open() as stream:
            await stream.send_message(request, end=True)
            reply: empty_pb2.Empty = await stream.recv_message()

    """
    # stream state
    _send_request_done = False
    _send_message_done = False
    _end_done = False
    _recv_initial_metadata_done = False
    _recv_trailing_metadata_done = False
    _cancel_done = False
    _trailers_only: Optional[bool] = None

    _stream: _Stream
    _release_stream: 'IReleaseStream'

    _wrapper_ctx = None

    #: This property contains initial metadata, received with headers from
    #: the server. It equals to ``None`` initially, and to a multi-dict object
    #: after :py:meth:`recv_initial_metadata` coroutine succeeds.
    initial_metadata: Optional[_Metadata] = None

    #: This property contains trailing metadata, received with trailers from
    #: the server. It equals to ``None`` initially, and to a multi-dict object
    #: after :py:meth:`recv_trailing_metadata` coroutine succeeds.
    trailing_metadata: Optional[_Metadata] = None

    #: Connection's peer info of type :py:class:`~grpclib.protocol.Peer`
    peer: Optional[Peer] = None

    # stats
    _messages_sent = 0
    _messages_received = 0

    def __init__(
        self,
        channel: 'Channel',
        method_name: str,
        metadata: _Metadata,
        cardinality: Cardinality,
        send_type: Type[_SendType],
        recv_type: Type[_RecvType],
        *,
        codec: CodecBase,
        status_details_codec: Optional[StatusDetailsCodecBase],
        dispatch: _DispatchChannelEvents,
        deadline: Optional[Deadline] = None,
    ) -> None:
        self._channel = channel
        self._method_name = method_name
        self._metadata = metadata
        self._cardinality = cardinality