import functools
from types import FunctionType
from types import ModuleType
from typing import Any
from typing import Dict
from typing import Optional
from typing import Tuple
from typing import cast

# This module should only be imported after django is imported
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse

from ddtrace import config
from ddtrace._trace.pin import Pin
from ddtrace.constants import SPAN_KIND
from ddtrace.contrib.internal import trace_utils
from ddtrace.contrib.internal.asgi.middleware import span_from_scope
from ddtrace.contrib.internal.django.compat import get_resolver
from ddtrace.contrib.internal.django.utils import REQUEST_DEFAULT_RESOURCE
from ddtrace.contrib.internal.django.utils import _after_request_tags
from ddtrace.contrib.internal.django.utils import _before_request_tags
from ddtrace.ext import SpanKind
from ddtrace.ext import SpanTypes
from ddtrace.ext import http
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.logger import get_logger
from ddtrace.internal.schema import schematize_url_operation
from ddtrace.internal.schema.span_attribute_schema import SpanDirection
from ddtrace.internal.utils import Block_config
from ddtrace.internal.utils import get_argument_value
from ddtrace.internal.utils import get_blocked
from ddtrace.internal.utils import http as http_utils
from ddtrace.internal.utils import set_blocked
from ddtrace.internal.wrapping import is_wrapped_with
from ddtrace.internal.wrapping import unwrap
from ddtrace.internal.wrapping import wrap
from ddtrace.settings.integration import IntegrationConfig

from . import utils


log = get_logger(__name__)

# PERF: cache the getattr lookup for the Django config
config_django: IntegrationConfig = cast(IntegrationConfig, config.django)


def _gather_block_metadata(request, request_headers, ctx: core.ExecutionContext):
    url: Optional[str] = None
    metadata: Dict[str, str] = {}
    query: str = ""
    try:
        metadata = {http.STATUS_CODE: "403", http.METHOD: request.method}
        url = utils.get_request_uri(request)
        query = request.META.get("QUERY_STRING", "")
        if query and config_django.trace_query_string:
            metadata[http.QUERY_STRING] = query
        user_agent = trace_utils._get_request_header_user_agent(request_headers)
        if user_agent:
            metadata[http.USER_AGENT] = user_agent
    except Exception as e:
        log.warning("Could not gather some metadata on blocked request: %s", str(e))
    core.dispatch("django.block_request_callback", (ctx, metadata, config_django, url, query))


def _block_request_callable(request, request_headers, ctx: core.ExecutionContext):
    # This is used by user-id blocking to block responses. It could be called
    # at any point so it's a callable stored in the ASM context.
    set_blocked()
    _gather_block_metadata(request, request_headers, ctx)
    raise PermissionDenied()


def traced_get_response(func: FunctionType, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any:
    """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations).

    This is the main entry point for requests.

    Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler).
    This method invokes the middleware chain and returns the response generated by the chain.
    """
    instance = args[0]

    request = get_argument_value(args, kwargs, 1, "request")
    if request is None:
        return func(*args, **kwargs)

    request_headers = utils._get_request_headers(request)

    pin = Pin.get_from(instance)

    with core.context_with_data(
        "django.traced_get_response",
        remote_addr=request.META.get("REMOTE_ADDR"),
        headers=request_headers,
        headers_case_sensitive=True,
        span_name=schematize_url_operation("django.request", protocol="http", direction=SpanDirection.INBOUND),
        resource=utils.REQUEST_DEFAULT_RESOURCE,
        service=trace_utils.int_service(pin, config_django),
        span_type=SpanTypes.WEB,
        tags={COMPONENT: config_django.integration_name, SPAN_KIND: SpanKind.SERVER},
        integration_config=config_django,
        distributed_headers=request_headers,
        activate_distributed_headers=True,
        # TODO: Migrate all tests to snapshot tests and remove this
        tracer=config_django._tracer,
    ) as ctx:
        core.dispatch(
            "django.traced_get_response.pre",
            (
                functools.partial(_block_request_callable, request, request_headers, ctx),
                ctx,
                request,
                utils._before_request_tags,
            ),
        )

        response = None

        def blocked_response(block_config: Block_config):
            if block_config.type == "none":
                response = HttpResponse("", status=block_config.status_code)
                if block_config.location:
                    response["location"] = block_config.location
            else:
                content = http_utils._get_blocked_template(block_config.content_type, block_config.block_id)
                response = HttpResponse(
                    content, content_type=block_config.content_type, status=block_config.status_code
                )
                response.content = content
                response["Content-Length"] = len(content.encode())
            utils._after_request_tags(pin, ctx.span, request, response)
            return response

        try:
            if block_config := get_blocked():
                response = blocked_response(block_config)
            else:
                query = request.META.get("QUERY_STRING", "")
                uri = utils.get_request_uri(request)
                if uri is not None and query:
                    uri += "?" + query
                resolver = get_resolver(getattr(request, "urlconf", None))
                if resolver:
                    try:
                        path = resolver.resolve(request.path_info).kwargs
                        log.debug("resolver.pattern %s", path)
                    except Exception:
                        path = None

                core.dispatch(
                    "django.start_response", (ctx, request, utils._extract_body, utils._remake_body, query, uri, path)
                )
                core.dispatch("django.start_response.post", ("Django",))

                if block_config := get_blocked():
                    response = blocked_response(block_config)
                else:
                    try:
                        response = func(*args, **kwargs)
                    except BlockingException as e:
                        set_blocked(e.args[0])
                        response = blocked_response(e.args[0])
                        return response

                    if block_config := get_blocked():
                        response = blocked_response(block_config)

        finally:
            core.dispatch("django.finalize_response.pre", (ctx, utils._after_request_tags, request, response))
            if not get_blocked():
                core.dispatch("django.finalize_response", ("Django",))
                if block_config := get_blocked():
                    response = blocked_response(block_config)
        return response


async def traced_get_response_async(
    func: FunctionType, instance: object, args: Tuple[Any, ...], kwargs: Dict[str, Any]
) -> Any:
    """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations).

    This is the main entry point for requests.

    Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler).
    This method invokes the middleware chain and returns the response generated by the chain.
    """
    pin = Pin.get_from(instance)

    request = get_argument_value(args, kwargs, 0, "request")
    span = span_from_scope(request.scope)
    if span is None:
        return await func(*args, **kwargs)

    # Reset the span resource so we can know if it was modified during the request or not
    span.resource = REQUEST_DEFAULT_RESOURCE
    _before_request_tags(pin, span, request)
    response = None
    try:
        response = await func(*args, **kwargs)
    finally:
        # DEV: Always set these tags, this is where `span.resource` is set
        _after_request_tags(pin, span, request, response)
    return response


def instrument_module(django: ModuleType, django_core_handlers_base: ModuleType) -> None:
    if not is_wrapped_with(django_core_handlers_base.BaseHandler.get_response, traced_get_response):
        wrap(
            django_core_handlers_base.BaseHandler.get_response,
            traced_get_response,
        )

    if django.VERSION >= (3, 1):
        # DEV: We cannot use bytecode wrappers here, otherwise in Python 3.13+ we'll trigger:
        #      `ValueError: coroutine already executing`
        if not trace_utils.iswrapped(django_core_handlers_base.BaseHandler, "get_response_async"):
            trace_utils.wrap(django_core_handlers_base.BaseHandler, "get_response_async", traced_get_response_async)


def uninstrument_module(django: ModuleType, django_core_handlers_base: ModuleType) -> None:
    if is_wrapped_with(django_core_handlers_base.BaseHandler.get_response, traced_get_response):
        unwrap(
            django_core_handlers_base.BaseHandler.get_response,
            traced_get_response,
        )

    if django.VERSION >= (3, 1):
        # DEV: We cannot use bytecode wrappers here, otherwise in Python 3.13+ we'll trigger:
        #      `ValueError: coroutine already executing`
        if trace_utils.iswrapped(django_core_handlers_base.BaseHandler, "get_response_async"):
            trace_utils.unwrap(
                django_core_handlers_base.BaseHandler,
                "get_response_async",
            )
