"""API for the plugin app."""

from typing import Optional

from django.core.exceptions import ValidationError
from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _

import django_filters.rest_framework.filters as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework import permissions, status
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.views import APIView

import InvenTree.permissions
import plugin.serializers as PluginSerializers
from InvenTree.api import meta_path
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.helpers import str2bool
from InvenTree.mixins import (
    CreateAPI,
    ListAPI,
    RetrieveAPI,
    RetrieveDestroyAPI,
    RetrieveUpdateAPI,
    UpdateAPI,
)
from plugin.base.action.api import ActionPluginView
from plugin.base.barcodes.api import barcode_api_urls
from plugin.base.locate.api import LocatePluginView
from plugin.base.supplier.api import supplier_api_urls
from plugin.base.ui.api import ui_plugins_api_urls
from plugin.models import PluginConfig, PluginSetting, PluginUserSetting
from plugin.plugin import InvenTreePlugin
from plugin.registry import registry


class PluginFilter(FilterSet):
    """Filter for the PluginConfig model.

    Provides custom filtering options for the FilterList API endpoint.
    """

    class Meta:
        """Meta for the filter."""

        model = PluginConfig
        fields = ['active']

    mixin = rest_filters.CharFilter(
        field_name='mixin', method='filter_mixin', label='Mixin'
    )

    def filter_mixin(self, queryset, name, value):
        """Filter by implement mixin.

        - A comma-separated list of mixin names can be provided.
        - Only plugins which implement all of the provided mixins will be returned.
        """
        matches = []
        mixins = [x.strip().lower() for x in value.split(',') if x]

        for result in queryset:
            match = True

            for mixin in mixins:
                if mixin not in result.mixins():
                    match = False
                    break

            if match:
                matches.append(result.pk)

        return queryset.filter(pk__in=matches)

    builtin = rest_filters.BooleanFilter(
        field_name='builtin', label=_('Builtin'), method='filter_builtin'
    )

    def filter_builtin(self, queryset, name, value):
        """Filter by 'builtin' flag."""
        matches = []

        for result in queryset:
            if result.is_builtin() == value:
                matches.append(result.pk)

        return queryset.filter(pk__in=matches)

    mandatory = rest_filters.BooleanFilter(
        field_name='mandatory', label=_('Mandatory'), method='filter_mandatory'
    )

    def filter_mandatory(self, queryset, name, value):
        """Filter by 'mandatory' flag."""
        from django.conf import settings

        mandatory_keys = [*registry.MANDATORY_PLUGINS, *settings.PLUGINS_MANDATORY]

        if str2bool(value):
            return queryset.filter(key__in=mandatory_keys)
        else:
            return queryset.exclude(key__in=mandatory_keys)

    sample = rest_filters.BooleanFilter(
        field_name='sample', label=_('Sample'), method='filter_sample'
    )

    def filter_sample(self, queryset, name, value):
        """Filter by 'sample' flag."""
        matches = []

        for result in queryset:
            if result.is_sample() == value:
                matches.append(result.pk)

        return queryset.filter(pk__in=matches)

    installed = rest_filters.BooleanFilter(
        field_name='installed', label=_('Installed'), method='filter_installed'
    )

    def filter_installed(self, queryset, name, value):
        """Filter by 'installed' flag."""
        matches = []

        for result in queryset:
            if result.is_installed() == value:
                matches.append(result.pk)

        return queryset.filter(pk__in=matches)


class PluginList(ListAPI):
    """API endpoint for list of PluginConfig objects.

    - GET: Return a list of all PluginConfig objects
    """

    # Allow any logged in user to read this endpoint
    # This is necessary to allow certain functionality,
    # e.g. determining which label printing plugins are available
    permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]

    filterset_class = PluginFilter

    serializer_class = PluginSerializers.PluginConfigSerializer
    queryset = PluginConfig.objects.all()

    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['key', 'name', 'active']

    ordering = ['-active', 'name', 'key']

    search_fields = ['key', 'name']


class PluginDetail(RetrieveDestroyAPI):
    """API detail endpoint for PluginConfig object.

    get:
    Return a single PluginConfig object

    post:
    Update a PluginConfig

    delete:
    Remove a PluginConfig
    """

    queryset = PluginConfig.objects.all()
    serializer_class = PluginSerializers.PluginConfigSerializer
    permission_classes = [
        permissions.IsAuthenticated,
        InvenTree.permissions.IsSuperuserOrReadOnlyOrScope,
    ]
    lookup_field = 'key'
    lookup_url_kwarg = 'plugin'

    def delete(self, request, *args, **kwargs):
        """Handle DELETE request for a PluginConfig instance.

        We only allow plugin deletion if the plugin is not active.
        """
        cfg = self.get_object()

        if cfg.active:
            raise ValidationError({
                'detail': _('Plugin cannot be deleted as it is currently active')
            })

        return super().delete(request, *args, **kwargs)


class PluginAdminDetail(RetrieveAPI):
    """Endpoint for viewing admin integration plugin details.

    This endpoint is used to view the available admin integration options for a plugin.
    """

    queryset = PluginConfig.objects.all()
    serializer_class = PluginSerializers.PluginAdminDetailSerializer
    permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
    lookup_field = 'key'
    lookup_url_kwarg = 'plugin'


class PluginInstall(CreateAPI):
    """Endpoint for installing a new plugin."""

    queryset = PluginConfig.objects.none()
    serializer_class = PluginSerializers.PluginConfigInstallSerializer
    permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]

    def create(self, request, *args, **kwargs):
        """Install a plugin via the API."""
        # Clean up input data
        data = self.clean_data(request.data)

        serializer = self.get_serializer(data=data)
        serializer.is_valid(raise_exception=True)
        result = self.perform_create(serializer)
        result['input'] = serializer.data
        headers = self.get_success_headers(serializer.data)
        return Response(result, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        """Saving the serializer instance performs plugin installation."""
        return serializer.save()


class PluginUninstall(UpdateAPI):
    """Endpoint for uninstalling a single plugin."""

    queryset = PluginConfig.objects.all()
    serializer_class = PluginSerializers.PluginUninstallSerializer
    permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
    lookup_field = 'key'
    lookup_url_kwarg = 'plugin'

    def perform_update(self, serializer):
        """Uninstall the plugin."""
        serializer.save()


class PluginActivate(UpdateAPI):
    """Endpoint for activating a plugin.

    - PATCH: Activate a plugin

    Pass a boolean value for the 'active' field.
    If not provided, it is assumed to be True,
    and the plugin will be activated.
    """

    queryset = PluginConfig.objects.all()
    serializer_class = PluginSerializers.PluginActivateSerializer
    permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
    lookup_field = 'key'
    lookup_url_kwarg = 'plugin'

    def get_object(self):
        """Returns the object for the view."""
        if self.request.data.get('pk', None):
            return self.queryset.get(pk=self.request.data.get('pk'))
        return super().get_object()

    def perform_update(self, serializer):
        """Activate the plugin."""
        serializer.save()


class PluginReload(CreateAPI):
    """Endpoint for reloading all plugins."""

    queryset = PluginConfig.objects.none()
    serializer_class = PluginSerializers.PluginReloadSerializer
    permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]

    def perform_create(self, serializer):
        """Saving the serializer instance performs plugin installation."""
        return serializer.save()


class PluginSettingList(ListAPI):
    """List endpoint for all plugin related settings.

    - read only
    - only accessible by staff users
    """

    queryset = PluginSetting.objects.all()
    serializer_class = PluginSerializers.PluginSettingSerializer

    permission_classes = [
        permissions.IsAuthenticated,
        InvenTree.permissions.GlobalSettingsPermissions,
    ]

    filter_backends = [DjangoFilterBackend]

    filterset_fields = ['plugin__active', 'plugin__key']

    @extend_schema(operation_id='plugins_settings_list_all')
    def get(self, request, *args, **kwargs):
        """List endpoint for all plugin related settings.

        - read only
        - only accessible by staff users
        """
        return super().get(request, *args, **kwargs)


def check_plugin(
    plugin_slug: Optional[str], plugin_pk: Optional[int]
) -> InvenTreePlugin:
    """Check that a plugin for the provided slug exists and get the config.

    Args:
        plugin_slug (str): Slug for plugin.
        plugin_pk (int): Primary key for plugin.

    Returns:
        InvenTreePlugin: The config object for the provided plugin.

    Raises:
        NotFound: If plugin is not installed
        NotFound: If plugin is not correctly registered
        NotFound: If plugin is not active
    """
    # Make sure that a plugin reference is specified
    if plugin_slug is None and plugin_pk is None:
        raise NotFound(detail='Plugin not specified')

    # Define filter
    filters = {}
    if plugin_slug:
        filters['key'] = plugin_slug
    elif plugin_pk:
        filters['pk'] = plugin_pk
    ref = plugin_slug or plugin_pk

    # Check that the 'plugin' specified is valid
    try:
        plugin_cfg = PluginConfig.objects.filter(**filters).first()
    except PluginConfig.DoesNotExist:
        raise NotFound(detail=f"Plugin '{ref}' not installed")

    if plugin_cfg is None:
        # This only occurs if the plugin mechanism broke
        raise NotFound(detail=f"Plugin '{ref}' not installed")  # pragma: no cover

    # Check that the plugin is activated
    if not plugin_cfg.active:
        raise NotFound(detail=f"Plugin '{ref}' is not active")

    plugin = plugin_cfg.plugin

    if not plugin:
        raise NotFound(detail=f"Plugin '{ref}' not installed")

    return plugin


class PluginAllSettingList(APIView):
    """List endpoint for all plugin settings for a specific plugin.

    - GET: return all settings for a plugin config
    """

    permission_classes = [
        permissions.IsAuthenticated,
        InvenTree.permissions.GlobalSettingsPermissions,
    ]

    @extend_schema(
        responses={200: PluginSerializers.PluginSettingSerializer(many=True)}
    )
    def get(self, request, plugin):
        """Get all settings for a plugin config."""
        # look up the plugin
        plugin = check_plugin(plugin, None)

        settings = getattr(plugin, 'settings', {})

        settings_dict = PluginSetting.all_settings(
            settings_definition=settings, plugin=plugin.plugin_config()
        )

        results = PluginSerializers.PluginSettingSerializer(
            list(settings_dict.values()), many=True
        ).data
        return Response(results)


class PluginSettingDetail(RetrieveUpdateAPI):
    """Detail endpoint for a plugin-specific setting."""

    queryset = PluginSetting.objects.all()
    serializer_class = PluginSerializers.PluginSettingSerializer

    permission_classes = [
        permissions.IsAuthenticated,
        InvenTree.permissions.GlobalSettingsPermissions,
    ]

    def get_object(self):
        """Lookup the plugin setting object, based on the URL.

        The URL provides the 'slug' of the plugin, and the 'key' of the setting.
        Both the 'slug' and 'key' must be valid, else a 404 error is raised
        """
        setting_key = self.kwargs['key']

        # Look up plugin
        plugin = check_plugin(self.kwargs.get('plugin', None), None)

        settings = getattr(plugin, 'settings', {})

        if setting_key not in settings:
            raise NotFound(
                detail=f"Plugin '{plugin.slug}' has no setting matching '{setting_key}'"
            )

        return PluginSetting.get_setting_object(
            setting_key, plugin=plugin.plugin_config()
        )


class PluginUserSettingList(APIView):
    """List endpoint for all user settings for a specific plugin.

    - GET: return all user settings for a plugin config
    """

    queryset = PluginUserSetting.objects.all()
    serializer_class = PluginSerializers.PluginUserSettingSerializer
    permission_classes = [InvenTree.permissions.UserSettingsPermissionsOrScope]

    @extend_schema(
        responses={200: PluginSerializers.PluginUserSettingSerializer(many=True)}
    )
    def get(self, request, plugin):
        """Get all user settings for a plugin config."""
        # look up the plugin
        plugin = check_plugin(plugin, None)

        user_settings = getattr(plugin, 'user_settings', {})

        settings_dict = PluginUserSetting.all_settings(
            settings_definition=user_settings,
            plugin=plugin.plugin_config(),
            user=request.user,
        )

        results = PluginSerializers.PluginUserSettingSerializer(
            list(settings_dict.values()), many=True
        ).data
        return Response(results)


class PluginUserSettingDetail(RetrieveUpdateAPI):
    """Detail endpoint for a plugin-specific user setting."""

    lookup_field = 'key'
    queryset = PluginUserSetting.objects.all()
    serializer_class = PluginSerializers.PluginUserSettingSerializer
    permission_classes = [InvenTree.permissions.UserSettingsPermissionsOrScope]

    def get_object(self):
        """Lookup the plugin user setting object, based on the URL."""
        setting_key = self.kwargs['key']

        # Look up plugin
        plugin = check_plugin(self.kwargs.get('plugin', None), None)

        settings = getattr(plugin, 'user_settings', {})

        if setting_key not in settings:
            raise NotFound(
                detail=f"Plugin '{plugin.slug}' has no user setting matching '{setting_key}'"
            )

        return PluginUserSetting.get_setting_object(
            setting_key, plugin=plugin.plugin_config(), user=self.request.user
        )


class RegistryStatusView(APIView):
    """Status API endpoint for the plugin registry.

    - GET: Provide status data for the plugin registry
    """

    permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]

    serializer_class = PluginSerializers.PluginRegistryStatusSerializer

    @extend_schema(responses={200: PluginSerializers.PluginRegistryStatusSerializer()})
    def get(self, request):
        """Show plugin registry status information."""
        error_list = []

        for stage, errors in registry.errors.items():
            for error_detail in errors:
                for name, message in error_detail.items():
                    error_list.append({
                        'stage': stage,
                        'name': name,
                        'message': message,
                    })

        result = PluginSerializers.PluginRegistryStatusSerializer({
            'registry_errors': error_list,
            'active_plugins': PluginConfig.objects.filter(active=True).count(),
        }).data

        return Response(result)


# class PluginMetadataView(MetadataView):
#     """Metadata API endpoint for the PluginConfig model."""

#     lookup_field = 'key'
#     lookup_url_kwarg = 'plugin'


plugin_api_urls = [
    path('action/', ActionPluginView.as_view(), name='api-action-plugin'),
    path('barcode/', include(barcode_api_urls)),
    path('locate/', LocatePluginView.as_view(), name='api-locate-plugin'),
    path(
        'plugins/',
        include([
            # UI plugins
            path('ui/', include(ui_plugins_api_urls)),
            # Plugin management
            path('reload/', PluginReload.as_view(), name='api-plugin-reload'),
            path('install/', PluginInstall.as_view(), name='api-plugin-install'),
            # Registry status
            path(
                'status/',
                RegistryStatusView.as_view(),
                name='api-plugin-registry-status',
            ),
            path(
                'settings/',
                include([
                    path(
                        '', PluginSettingList.as_view(), name='api-plugin-setting-list'
                    )
                ]),
            ),
            # Lookup for individual plugins (based on 'plugin', not 'pk')
            path(
                '<str:plugin>/',
                include([
                    path(
                        'user-settings/',
                        include([
                            re_path(
                                r'^(?P<key>\w+)/',
                                PluginUserSettingDetail.as_view(),
                                name='api-plugin-user-setting-detail',
                            ),
                            path(
                                '',
                                PluginUserSettingList.as_view(),
                                name='api-plugin-user-setting-list',
                            ),
                        ]),
                    ),
                    path(
                        'settings/',
                        include([
                            re_path(
                                r'^(?P<key>\w+)/',
                                PluginSettingDetail.as_view(),
                                name='api-plugin-setting-detail',
                            ),
                            path(
                                '',
                                PluginAllSettingList.as_view(),
                                name='api-plugin-settings',
                            ),
                        ]),
                    ),
                    meta_path(
                        PluginConfig, lookup_field='key', lookup_field_ref='plugin'
                    ),
                    path(
                        'activate/',
                        PluginActivate.as_view(),
                        name='api-plugin-detail-activate',
                    ),
                    path(
                        'uninstall/',
                        PluginUninstall.as_view(),
                        name='api-plugin-uninstall',
                    ),
                    path(
                        'admin/', PluginAdminDetail.as_view(), name='api-plugin-admin'
                    ),
                    path('', PluginDetail.as_view(), name='api-plugin-detail'),
                ]),
            ),
            path('', PluginList.as_view(), name='api-plugin-list'),
        ]),
    ),
    path('supplier/', include(supplier_api_urls)),
]
