import json
from collections import namedtuple
from importlib import import_module
from typing import Any, Dict, List, Optional, Type

from django.conf import settings
from django.templatetags.static import static
from django.utils import translation
from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.settings import api_settings
from rest_framework.views import APIView

from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.plumbing import get_relative_url, set_query_parameters
from drf_spectacular.renderers import (
    OpenApiJsonRenderer, OpenApiJsonRenderer2, OpenApiYamlRenderer, OpenApiYamlRenderer2,
)
from drf_spectacular.settings import patched_settings, spectacular_settings
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema

if spectacular_settings.SERVE_INCLUDE_SCHEMA:
    SCHEMA_KWARGS: Dict[str, Any] = {'responses': {200: OpenApiTypes.OBJECT}}

    if settings.USE_I18N:
        SCHEMA_KWARGS['parameters'] = [
            OpenApiParameter(
                'lang', str, OpenApiParameter.QUERY, enum=list(dict(settings.LANGUAGES).keys())
            )
        ]
else:
    SCHEMA_KWARGS = {'exclude': True}

if spectacular_settings.SERVE_AUTHENTICATION is not None:
    AUTHENTICATION_CLASSES = spectacular_settings.SERVE_AUTHENTICATION
else:
    AUTHENTICATION_CLASSES = api_settings.DEFAULT_AUTHENTICATION_CLASSES


class SpectacularAPIView(APIView):
    __doc__ = _("""
    OpenApi3 schema for this API. Format can be selected via content negotiation.

    - YAML: application/vnd.oai.openapi
    - JSON: application/vnd.oai.openapi+json
    """)  # type: ignore
    renderer_classes = [
        OpenApiYamlRenderer, OpenApiYamlRenderer2, OpenApiJsonRenderer, OpenApiJsonRenderer2
    ]
    permission_classes = spectacular_settings.SERVE_PERMISSIONS
    authentication_classes = AUTHENTICATION_CLASSES
    generator_class: Type[SchemaGenerator] = spectacular_settings.DEFAULT_GENERATOR_CLASS
    serve_public: bool = spectacular_settings.SERVE_PUBLIC
    urlconf: Optional[str] = spectacular_settings.SERVE_URLCONF
    api_version: Optional[str] = None
    custom_settings: Optional[Dict[str, Any]] = None
    patterns: Optional[List[Any]] = None

    @extend_schema(**SCHEMA_KWARGS)
    async def get(self, request, *args, **kwargs):
        # special handling of custom urlconf parameter
        if isinstance(self.urlconf, list) or isinstance(self.urlconf, tuple):
            ModuleWrapper = namedtuple('ModuleWrapper', ['urlpatterns'])
            if all(isinstance(i, str) for i in self.urlconf):
                # list of import string for urlconf
                patterns = []
                for item in self.urlconf:
                    url = import_module(item)
                    patterns += url.urlpatterns
                self.urlconf = ModuleWrapper(tuple(patterns))
            else:
                # explicitly resolved urlconf
                self.urlconf = ModuleWrapper(tuple(self.urlconf))

        with patched_settings(self.custom_settings):
            if settings.USE_I18N and request.GET.get('lang'):
                with translation.override(request.GET.get('lang')):
                    return self._get_schema_response(request)
            else:
                return self._get_schema_response(request)

    async def _get_schema_response(self, request):
        # version specified as parameter to the view always takes precedence. after
        # that we try to source version through the schema view's own versioning_class.
        version = self.api_version or request.version or self._get_version_parameter(request)
        generator = self.generator_class(urlconf=self.urlconf, api_version=version, patterns=self.patterns)
        return Response(
            data=generator.get_schema(request=request, public=self.serve_public),
            headers={"Content-Disposition": f'inline; filename="{self._get_filename(request, version)}"'}
        )

    async def _get_filename(self, request, version):
        return "{title}{version}.{suffix}".format(
            title=spectacular_settings.TITLE or 'schema',
            version=f' ({version})' if version else '',
            suffix=self.perform_content_negotiation(request, force=True)[0].format
        )

    async def _get_version_parameter(self, request):
        version = request.GET.get('version')
        if not api_settings.ALLOWED_VERSIONS or version in api_settings.ALLOWED_VERSIONS:
            return version
        return None


class SpectacularYAMLAPIView(SpectacularAPIView):
    renderer_classes = [OpenApiYamlRenderer, OpenApiYamlRenderer2]


class SpectacularJSONAPIView(SpectacularAPIView):
    renderer_classes = [OpenApiJsonRenderer, OpenApiJsonRenderer2]


async def _get_sidecar_url(filepath):
    return static(f'drf_spectacular_sidecar/{filepath}')


class SpectacularSwaggerView(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    permission_classes = spectacular_settings.SERVE_PERMISSIONS
    authentication_classes = AUTHENTICATION_CLASSES
    url_name: str = 'schema'
    url: Optional[str] = None
    template_name: str = 'drf_spectacular/swagger_ui.html'
    template_name_js: str = 'drf_spectacular/swagger_ui.js'
    title: str = spectacular_settings.TITLE

    @extend_schema(exclude=True)
    async def get(self, request, *args, **kwargs):
        return Response(
            data={
                'title': self.title,
                'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
                'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
                'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
                'favicon_href': self._swagger_ui_favicon(),
                'schema_url': self._get_schema_url(request),
                'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
                'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG),
                'template_name_js': self.template_name_js,
                'csrf_header_name': self._get_csrf_header_name(),
                'schema_auth_names': self._dump(self._get_schema_auth_names()),
            },
            template_name=self.template_name,
            headers={
                "Cross-Origin-Opener-Policy": "unsafe-none",
            }
        )

    async def _dump(self, data):
        return data if isinstance(data, str) else json.dumps(data, indent=2)

    async def _get_schema_url(self, request):
        schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
        return set_query_parameters(
            url=schema_url,
            lang=request.GET.get('lang'),
            version=request.GET.get('version')
        )

    async def _get_csrf_header_name(self):
        csrf_header_name = settings.CSRF_HEADER_NAME
        if csrf_header_name.startswith('HTTP_'):
            csrf_header_name = csrf_header_name[5:]
        return csrf_header_name.replace('_', '-')

    async def _get_schema_auth_names(self):
        from drf_spectacular.extensions import OpenApiAuthenticationExtension
        if spectacular_settings.SERVE_PUBLIC:
            return []
        auth_extensions = [
            OpenApiAuthenticationExtension.get_match(klass)
            for klass in self.authentication_classes
        ]
        return [auth.name for auth in auth_extensions if auth]

    @staticmethod
    async def _swagger_ui_resource(filename):
        if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR':
            return _get_sidecar_url(f'swagger-ui-dist/{filename}')
        return f'{spectacular_settings.SWAGGER_UI_DIST}/{filename}'

    @staticmethod
    async def _swagger_ui_favicon():
        if spectacular_settings.SWAGGER_UI_FAVICON_HREF == 'SIDECAR':
            return _get_sidecar_url('swagger-ui-dist/favicon-32x32.png')
        return spectacular_settings.SWAGGER_UI_FAVICON_HREF


class SpectacularSwaggerSplitView(SpectacularSwaggerView):
    """
    Alternate Swagger UI implementation that separates the html request from the
    javascript request to cater to web servers with stricter CSP policies.
    """
    url_self: Optional[str] = None

    @extend_schema(exclude=True)
    async def get(self, request, *args, **kwargs):
        if request.GET.get('script') is not None:
            return Response(
                data={
                    'schema_url': self._get_schema_url(request),
                    'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
                    'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG),
                    'csrf_header_name': self._get_csrf_header_name(),
                    'schema_auth_names': self._dump(self._get_schema_auth_names()),
                },
                template_name=self.template_name_js,
                content_type='application/javascript',
            )
        else:
            script_url = self.url_self or request.get_full_path()
            return Response(
                data={
                    'title': self.title,
                    'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
                    'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
                    'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
                    'favicon_href': self._swagger_ui_favicon(),
                    'script_url': set_query_parameters(
                        url=script_url,
                        lang=request.GET.get('lang'),
                        script=''  # signal to deliver init script
                    )
                },
                template_name=self.template_name,
            )


class SpectacularRedocView(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    permission_classes = spectacular_settings.SERVE_PERMISSIONS
    authentication_classes = AUTHENTICATION_CLASSES
    url_name: str = 'schema'
    url: Optional[str] = None
    template_name: str = 'drf_spectacular/redoc.html'
    title: Optional[str] = spectacular_settings.TITLE

    @extend_schema(exclude=True)
    async def get(self, request, *args, **kwargs):
        return Response(
            data={
                'title': self.title,
                'redoc_standalone': self._redoc_standalone(),
                'schema_url': self._get_schema_url(request),
                'settings': self._dump(spectacular_settings.REDOC_UI_SETTINGS),
            },
            template_name=self.template_name
        )

    async def _dump(self, data):
        if not data:
            return None
        elif isinstance(data, str):
            return data
        else:
            return json.dumps(data, indent=2)

    @staticmethod
    async def _redoc_standalone():
        if spectacular_settings.REDOC_DIST == 'SIDECAR':
            return _get_sidecar_url('redoc/bundles/redoc.standalone.js')
        return f'{spectacular_settings.REDOC_DIST}/bundles/redoc.standalone.js'

    async def _get_schema_url(self, request):
        schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
        return set_query_parameters(
            url=schema_url,
            lang=request.GET.get('lang'),
            version=request.GET.get('version')
        )


class SpectacularSwaggerOauthRedirectView(RedirectView):
    """
    A view that serves the SwaggerUI oauth2-redirect.html file so that SwaggerUI can authenticate itself using Oauth2

    This view should be served as ``./oauth2-redirect.html`` relative to the SwaggerUI itself.
    If that is not possible, this views absolute url can also be set via the
    ``SPECTACULAR_SETTINGS.SWAGGER_UI_SETTINGS.oauth2RedirectUrl`` django settings.
    """
    async def get_redirect_url(self, *args, **kwargs):
        return _get_sidecar_url("swagger-ui-dist/oauth2-redirect.html") + "?" + self.request.GET.urlencode()
