import weakref
import contextlib
from inspect import iscoroutinefunction

import sentry_sdk
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import TransactionSource
from sentry_sdk.utils import (
    HAS_REAL_CONTEXTVARS,
    CONTEXTVARS_ERROR_MESSAGE,
    ensure_integration_enabled,
    event_from_exception,
    capture_internal_exceptions,
    transaction_from_function,
)
from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
from sentry_sdk.integrations._wsgi_common import (
    RequestExtractor,
    _filter_headers,
    _is_json_content_type,
)
from sentry_sdk.integrations.logging import ignore_logger

try:
    from tornado import version_info as TORNADO_VERSION
    from tornado.web import RequestHandler, HTTPError
    from tornado.gen import coroutine
except ImportError:
    raise DidNotEnable("Tornado not installed")

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Any
    from typing import Optional
    from typing import Dict
    from typing import Callable
    from typing import Generator

    from sentry_sdk._types import Event, EventProcessor


class TornadoIntegration(Integration):
    identifier = "tornado"
    origin = f"auto.http.{identifier}"

    @staticmethod
    def setup_once() -> None:
        _check_minimum_version(TornadoIntegration, TORNADO_VERSION)

        if not HAS_REAL_CONTEXTVARS:
            # Tornado is async. We better have contextvars or we're going to leak
            # state between requests.
            raise DidNotEnable(
                "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package"
                + CONTEXTVARS_ERROR_MESSAGE
            )

        ignore_logger("tornado.access")

        old_execute = RequestHandler._execute

        awaitable = iscoroutinefunction(old_execute)

        if awaitable:
            # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
            # In that case our method should be a coroutine function too
            async def sentry_execute_request_handler(
                self: "RequestHandler", *args: "Any", **kwargs: "Any"
            ) -> "Any":
                with _handle_request_impl(self):
                    return await old_execute(self, *args, **kwargs)

        else:

            @coroutine  # type: ignore
            def sentry_execute_request_handler(
                self: "RequestHandler", *args: "Any", **kwargs: "Any"
            ) -> "Any":
                with _handle_request_impl(self):
                    result = yield from old_execute(self, *args, **kwargs)
                    return result

        RequestHandler._execute = sentry_execute_request_handler

        old_log_exception = RequestHandler.log_exception

        def sentry_log_exception(
            self: "Any",
            ty: type,
            value: BaseException,
            tb: "Any",
            *args: "Any",
            **kwargs: "Any",
        ) -> "Optional[Any]":
            _capture_exception(ty, value, tb)
            return old_log_exception(self, ty, value, tb, *args, **kwargs)

        RequestHandler.log_exception = sentry_log_exception


@contextlib.contextmanager
def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]":
    integration = sentry_sdk.get_client().get_integration(TornadoIntegration)

    if integration is None:
        yield

    weak_handler = weakref.ref(self)

    with sentry_sdk.isolation_scope() as scope:
        headers = self.request.headers

        scope.clear_breadcrumbs()
        processor = _make_event_processor(weak_handler)
        scope.add_event_processor(processor)

        transaction = continue_trace(
            headers,
            op=OP.HTTP_SERVER,
            # Like with all other integrations, this is our
            # fallback transaction in case there is no route.
            # sentry_urldispatcher_resolve is responsible for
            # setting a transaction name later.
            name="generic Tornado request",
            source=TransactionSource.ROUTE,
            origin=TornadoIntegration.origin,
        )

        with sentry_sdk.start_transaction(
            transaction, custom_sampling_context={"tornado_request": self.request}
        ):
            yield


@ensure_integration_enabled(TornadoIntegration)
def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None:
    if isinstance(value, HTTPError):
        return

    event, hint = event_from_exception(
        (ty, value, tb),
        client_options=sentry_sdk.get_client().options,
        mechanism={"type": "tornado", "handled": False},
    )

    sentry_sdk.capture_event(event, hint=hint)


def _make_event_processor(
    weak_handler: "Callable[[], RequestHandler]",
) -> "EventProcessor":
    def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event":
        handler = weak_handler()
        if handler is None:
            return event

        request = handler.request

        with capture_internal_exceptions():
            method = getattr(handler, handler.request.method.lower())
            event["transaction"] = transaction_from_function(method) or ""
            event["transaction_info"] = {"source": TransactionSource.COMPONENT}

        with capture_internal_exceptions():
            extractor = TornadoRequestExtractor(request)
            extractor.extract_into_event(event)

            request_info = event["request"]

            request_info["url"] = "%s://%s%s" % (
                request.protocol,
                request.host,
                request.path,
            )

            request_info["query_string"] = request.query
            request_info["method"] = request.method
            request_info["env"] = {"REMOTE_ADDR": request.remote_ip}
            request_info["headers"] = _filter_headers(dict(request.headers))

        if should_send_default_pii():
            try:
                current_user = handler.current_user
            except Exception:
                current_user = None

            if current_user:
                event.setdefault("user", {}).setdefault("is_authenticated", True)

        return event

    return tornado_processor


class TornadoRequestExtractor(RequestExtractor):
    def content_length(self) -> int:
        if self.request.body is None:
            return 0
        return len(self.request.body)

    def cookies(self) -> "Dict[str, str]":
        return {k: v.value for k, v in self.request.cookies.items()}

    def raw_data(self) -> bytes:
        return self.request.body

    def form(self) -> "Dict[str, Any]":
        return {
            k: [v.decode("latin1", "replace") for v in vs]
            for k, vs in self.request.body_arguments.items()
        }

    def is_json(self) -> bool:
        return _is_json_content_type(self.request.headers.get("content-type"))

    def files(self) -> "Dict[str, Any]":
        return {k: v[0] for k, v in self.request.files.items() if v}

    def size_of_file(self, file: "Any") -> int:
        return len(file.body or ())
