"""Provides a JSON API for common components."""

import json
import json.decoder

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db.models import Q
from django.http import JsonResponse
from django.http.response import HttpResponse
from django.urls import include, path, re_path
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import csrf_exempt

import django_filters.rest_framework.filters as rest_filters
import django_q.models
import django_q.tasks
from django_filters.rest_framework.filterset import FilterSet
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from drf_spectacular.utils import (
    OpenApiParameter,
    OpenApiResponse,
    extend_schema,
    extend_schema_view,
)
from error_report.models import Error
from opentelemetry import trace
from pint._typing import UnitLike
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from sql_util.utils import SubqueryCount
from taggit.models import Tag

import common.filters
import common.models
import common.serializers
import InvenTree.conversion
import InvenTree.models
import InvenTree.ready
from common.icons import get_icon_packs
from common.settings import get_global_setting
from data_exporter.mixins import DataExportViewMixin
from generic.states.api import urlpattern as generic_states_api_urls
from InvenTree.api import (
    BulkCreateMixin,
    BulkDeleteMixin,
    BulkDeleteViewsetMixin,
    GenericMetadataView,
    SimpleGenericMetadataView,
    meta_path,
)
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
from InvenTree.helpers import inheritors, str2bool
from InvenTree.helpers_api import (
    InvenTreeApiRouter,
    RetrieveDestroyModelViewSet,
    RetrieveUpdateDestroyModelViewSet,
)
from InvenTree.helpers_email import send_email
from InvenTree.mixins import (
    CreateAPI,
    ListAPI,
    ListCreateAPI,
    OutputOptionsMixin,
    RetrieveAPI,
    RetrieveUpdateAPI,
    RetrieveUpdateDestroyAPI,
)
from InvenTree.permissions import (
    AllowAnyOrReadScope,
    GlobalSettingsPermissions,
    IsAdminOrAdminScope,
    IsAuthenticatedOrReadScope,
    IsStaffOrReadOnlyScope,
    IsSuperuserOrSuperScope,
    UserSettingsPermissionsOrScope,
)
from InvenTree.serializers import EmptySerializer

admin_router = InvenTreeApiRouter()
common_router = InvenTreeApiRouter()


class CsrfExemptMixin:
    """Exempts the view from CSRF requirements."""

    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        """Overwrites dispatch to be exempt from CSRF checks."""
        return super().dispatch(*args, **kwargs)


class WebhookView(CsrfExemptMixin, APIView):
    """Endpoint for receiving webhooks."""

    authentication_classes = []
    permission_classes = []
    model_class = common.models.WebhookEndpoint
    run_async = False
    serializer_class = None

    @extend_schema(
        responses={
            200: OpenApiResponse(
                description='Any data can be posted to the endpoint - everything will be passed to the WebhookEndpoint model.'
            )
        }
    )
    def post(self, request, endpoint, *args, **kwargs):
        """Process incoming webhook."""
        # get webhook definition
        self._get_webhook(endpoint, request, *args, **kwargs)

        # check headers
        headers = request.headers
        try:
            payload = json.loads(request.body)
        except json.decoder.JSONDecodeError as error:
            raise NotAcceptable(error.msg)

        # validate
        self.webhook.validate_token(payload, headers, request)
        # process data
        message = self.webhook.save_data(payload, headers, request)
        if self.run_async:
            django_q.tasks.async_task(self._process_payload, message.id)
        else:
            self._process_result(
                self.webhook.process_payload(message, payload, headers), message
            )

        data = self.webhook.get_return(payload, headers, request)
        return HttpResponse(data)

    def _process_payload(self, message_id):
        message = common.models.WebhookMessage.objects.get(message_id=message_id)
        self._process_result(
            self.webhook.process_payload(message, message.body, message.header), message
        )

    def _process_result(self, result, message):
        if result:
            message.worked_on = result
            message.save()
        else:
            message.delete()

    def _escalate_object(self, obj):
        classes = inheritors(obj.__class__)
        for cls in classes:
            mdl_name = cls._meta.model_name
            if hasattr(obj, mdl_name):
                return getattr(obj, mdl_name)
        return obj

    def _get_webhook(self, endpoint, request, *args, **kwargs):
        try:
            webhook = self.model_class.objects.get(endpoint_id=endpoint)
            self.webhook = self._escalate_object(webhook)
            self.webhook.init(request, *args, **kwargs)
            return self.webhook.process_webhook()
        except self.model_class.DoesNotExist:
            raise NotFound()


class CurrencyViewSet(viewsets.GenericViewSet):
    """Viewset for currency exchange information."""

    permission_classes = [IsAuthenticatedOrReadScope]
    serializer_class = EmptySerializer

    @action(
        detail=False,
        methods=['get'],
        serializer_class=common.serializers.CurrencyExchangeSerializer,
    )
    def exchange(self, request, fmt=None):
        """Return information on available currency conversions."""
        # Extract a list of all available rates
        try:
            rates = Rate.objects.all()
        except Exception:
            rates = []

        # Information on last update
        try:
            backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')

            if backend.exists():
                backend = backend.first()
                updated = backend.last_update
            else:
                updated = None
        except Exception:
            updated = None

        response = {
            'base_currency': get_global_setting(
                'INVENTREE_DEFAULT_CURRENCY', backup_value='USD'
            ),
            'exchange_rates': {},
            'updated': updated,
        }

        for rate in rates:
            response['exchange_rates'][rate.currency] = rate.value

        return Response(response)

    @action(
        detail=False,
        methods=['post'],
        permission_classes=[IsAuthenticatedOrReadScope, IsAdminUser],
    )
    def refresh(self, request, *args, **kwargs):
        """Performing a POST request will update currency exchange rates."""
        from InvenTree.tasks import update_exchange_rates

        update_exchange_rates(force=True)

        return Response({'success': 'Exchange rates updated'})


common_router.register('currency', CurrencyViewSet, basename='api-currency')


class SettingsList(ListAPI):
    """Generic ListView for settings.

    This is inherited by all list views for settings.
    """

    filter_backends = SEARCH_ORDER_FILTER

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

    search_fields = ['key']


class GlobalSettingsList(SettingsList):
    """API endpoint for accessing a list of global settings objects."""

    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
    serializer_class = common.serializers.GlobalSettingsSerializer
    permission_classes = [IsAuthenticated, GlobalSettingsPermissions]

    def list(self, request, *args, **kwargs):
        """Ensure all global settings are created."""
        common.models.InvenTreeSetting.build_default_values()
        return super().list(request, *args, **kwargs)


class GlobalSettingsDetail(RetrieveUpdateAPI):
    """Detail view for an individual "global setting" object.

    - User must have 'staff' status to view / edit
    """

    lookup_field = 'key'
    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
    serializer_class = common.serializers.GlobalSettingsSerializer
    permission_classes = [IsAuthenticated, GlobalSettingsPermissions]

    def get_object(self):
        """Attempt to find a global setting object with the provided key."""
        key = str(self.kwargs['key']).upper()

        if key.startswith('_') or key not in common.models.InvenTreeSetting.SETTINGS:
            raise NotFound()

        return common.models.InvenTreeSetting.get_setting_object(
            key, cache=False, create=True
        )


class UserSettingsList(SettingsList):
    """API endpoint for accessing a list of user settings objects."""

    queryset = common.models.InvenTreeUserSetting.objects.all()
    serializer_class = common.serializers.UserSettingsSerializer
    permission_classes = [UserSettingsPermissionsOrScope]

    def list(self, request, *args, **kwargs):
        """Ensure all user settings are created."""
        common.models.InvenTreeUserSetting.build_default_values(user=request.user)
        return super().list(request, *args, **kwargs)

    def get_queryset(self):
        """Return prefetched queryset."""
        queryset = super().get_queryset()
        queryset = queryset.prefetch_related('user')

        return queryset

    def filter_queryset(self, queryset):
        """Only list settings which apply to the current user."""
        try:
            user = self.request.user
        except AttributeError:  # pragma: no cover
            return common.models.InvenTreeUserSetting.objects.none()

        queryset = super().filter_queryset(queryset)

        if not user.is_authenticated:  # pragma: no cover
            raise PermissionDenied('User must be authenticated to access user settings')

        queryset = queryset.filter(user=user)

        return queryset


class UserSettingsDetail(RetrieveUpdateAPI):
    """Detail view for an individual "user setting" object.

    - User can only view / edit settings their own settings objects
    """

    lookup_field = 'key'
    queryset = common.models.InvenTreeUserSetting.objects.all()
    serializer_class = common.serializers.UserSettingsSerializer
    permission_classes = [UserSettingsPermissionsOrScope]

    def get_object(self):
        """Attempt to find a user setting object with the provided key."""
        key = str(self.kwargs['key']).upper()

        if (
            key.startswith('_')
            or key not in common.models.InvenTreeUserSetting.SETTINGS
        ):
            raise NotFound()

        return common.models.InvenTreeUserSetting.get_setting_object(
            key, user=self.request.user, cache=False, create=True
        )


class NotificationMessageViewSet(
    BulkDeleteViewsetMixin, RetrieveUpdateDestroyModelViewSet
):
    """Notifications for the current user.

    - User can only view / delete their own notification objects
    """

    queryset = common.models.NotificationMessage.objects.all()
    serializer_class = common.serializers.NotificationMessageSerializer
    permission_classes = [UserSettingsPermissionsOrScope]

    filter_backends = SEARCH_ORDER_FILTER
    ordering_fields = ['category', 'name', 'read', 'creation']
    search_fields = ['name', 'message']
    filterset_fields = ['category', 'read']

    def get_queryset(self):
        """Return prefetched queryset."""
        queryset = (
            super()
            .get_queryset()
            .prefetch_related(
                'source_content_type',
                'source_object',
                'target_content_type',
                'target_object',
                'user',
            )
        )

        return queryset

    def filter_queryset(self, queryset):
        """Only list notifications which apply to the current user."""
        try:
            user = self.request.user
        except AttributeError:
            return common.models.NotificationMessage.objects.none()

        queryset = super().filter_queryset(queryset)

        if not user.is_authenticated:  # pragma: no cover
            raise PermissionDenied('User must be authenticated to access notifications')

        queryset = queryset.filter(user=user)
        return queryset

    def filter_delete_queryset(self, queryset, request):
        """Ensure that the user can only delete their *own* notifications."""
        queryset = queryset.filter(user=request.user)
        return queryset

    @action(
        detail=False, methods=['post'], permission_classes=[IsAuthenticatedOrReadScope]
    )
    def readall(self, request, *args, **kwargs):
        """Set all messages for the current user as read."""
        try:
            self.queryset.filter(user=request.user, read=False).update(read=True)
            return Response({'status': 'ok'})
        except Exception as exc:
            raise serializers.ValidationError(
                detail=serializers.as_serializer_error(exc)
            )


common_router.register(
    'notifications', NotificationMessageViewSet, basename='api-notifications'
)


class NewsFeedViewSet(BulkDeleteViewsetMixin, RetrieveUpdateDestroyModelViewSet):
    """Newsfeed from the official inventree.org website."""

    queryset = common.models.NewsFeedEntry.objects.all()
    serializer_class = common.serializers.NewsFeedEntrySerializer
    permission_classes = [IsAdminOrAdminScope]

    filter_backends = ORDER_FILTER
    ordering = '-published'
    ordering_fields = ['published', 'author', 'read']
    filterset_fields = ['read']


common_router.register('news', NewsFeedViewSet, basename='api-news')


@extend_schema_view(
    retrieve=extend_schema(
        parameters=[
            OpenApiParameter(
                name='key',
                description='Unique identifier for this configuration',
                required=True,
                location=OpenApiParameter.PATH,
            )
        ]
    )
)
class ConfigViewSet(viewsets.ReadOnlyModelViewSet):
    """All accessed/in-use configurations."""

    queryset = CONFIG_LOOKUPS
    serializer_class = common.serializers.ConfigSerializer
    permission_classes = [IsSuperuserOrSuperScope]
    lookup_field = 'key'

    # Specifically disable pagination for this view
    pagination_class = None

    def get_object(self):
        """Attempt to find a config object with the provided key."""
        key = self.kwargs['key']
        value = CONFIG_LOOKUPS.get(key, None)
        if not value:
            raise NotFound()
        return {key: value}


admin_router.register('config', ConfigViewSet, basename='api-config')


class NotesImageFilter(FilterSet):
    """Filterset for the NotesImage API endpoint."""

    class Meta:
        """Metaclass options."""

        model = common.models.NotesImage
        fields = ['user', 'note']

    model_id = rest_filters.NumberFilter(
        label=_('Model ID'), field_name='note__model_id'
    )

    model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')

    def filter_model_type(self, queryset, name, value):
        """Filter queryset to include only Parameters of the given model type."""
        return common.filters.filter_content_type(
            queryset, 'note__model_type', value, allow_null=False
        )


class NotesImageList(ListCreateAPI):
    """List view for all notes images."""

    queryset = common.models.NotesImage.objects.all()
    serializer_class = common.serializers.NotesImageSerializer
    permission_classes = [IsAuthenticatedOrReadScope]
    filterset_class = NotesImageFilter

    filter_backends = SEARCH_ORDER_FILTER

    def perform_create(self, serializer):
        """Create (upload) a new notes image."""
        serializer.save(user=self.request.user)


class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
    """List view for all project codes."""

    queryset = common.models.ProjectCode.objects.all()
    serializer_class = common.serializers.ProjectCodeSerializer
    permission_classes = [IsStaffOrReadOnlyScope]
    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['code']
    filterset_fields = ['active']
    search_fields = ['code', 'description']


class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
    """Detail view for a particular project code."""

    queryset = common.models.ProjectCode.objects.all()
    serializer_class = common.serializers.ProjectCodeSerializer
    permission_classes = [IsStaffOrReadOnlyScope]


class TagFilter(FilterSet):
    """Custom filters for the TagList API endpoint."""

    class Meta:
        """Metaclass options for the filterset."""

        model = Tag
        fields = []

    model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')

    def filter_model_type(self, queryset, name, value):
        """Filter to tags which have been applied to the given model type."""
        ct = common.filters.determine_content_type(value)

        if ct is None:
            raise ValidationError({'model_type': f'Invalid model type: {value}'})

        return queryset.filter(taggit_taggeditem_items__content_type=ct).distinct()


class TagMixin:
    """Mixin class for Tag views."""

    serializer_class = common.serializers.TagSerializer
    queryset = Tag.objects.all()
    permission_classes = [IsStaffOrReadOnlyScope]


class TagList(TagMixin, ListCreateAPI):
    """List view for all tags."""

    filterset_class = TagFilter
    filter_backends = SEARCH_ORDER_FILTER
    ordering_fields = ['name']
    search_fields = ['name']


class TagDetail(TagMixin, RetrieveUpdateDestroyAPI):
    """Detail view for a particular tag."""


class CustomUnitViewset(DataExportViewMixin, viewsets.ModelViewSet):
    """List view for custom units."""

    queryset = common.models.CustomUnit.objects.all()
    serializer_class = common.serializers.CustomUnitSerializer
    permission_classes = [IsStaffOrReadOnlyScope]
    filter_backends = SEARCH_ORDER_FILTER

    @action(
        detail=False,
        methods=['get'],
        serializer_class=common.serializers.AllUnitListResponseSerializer,
    )
    def all(self, request, *args, **kwargs):
        """Return a list of all available units."""
        reg = InvenTree.conversion.get_unit_registry()
        all_units = {k: self.get_unit(reg, k) for k in reg}
        data = {
            'default_system': reg.default_system,
            'available_systems': dir(reg.sys),
            'available_units': {k: v for k, v in all_units.items() if v},
        }
        return Response(data)

    def get_unit(self, reg, k):
        """Parse a unit from the registry."""
        if not hasattr(reg, k):
            return None
        unit: type[UnitLike] = getattr(reg, k)
        return {
            'name': k,
            'is_alias': reg.get_name(k) == k,
            'compatible_units': [str(a) for a in unit.compatible_units()],  # ty:ignore[missing-argument]
            'isdimensionless': unit.dimensionless,
        }


common_router.register('units', CustomUnitViewset, basename='api-custom-unit')


class ErrorMessageViewSet(BulkDeleteViewsetMixin, RetrieveUpdateDestroyModelViewSet):
    """List view for server error messages."""

    queryset = Error.objects.all()
    serializer_class = common.serializers.ErrorMessageSerializer
    permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]

    filter_backends = SEARCH_ORDER_FILTER

    ordering = '-when'

    ordering_fields = ['when', 'info']

    search_fields = ['info', 'data']


common_router.register('error-report', ErrorMessageViewSet, basename='api-error')


class BackgroundTaskDetail(APIView):
    """Detail view for a single background task."""

    permission_classes = [IsAuthenticatedOrReadScope]

    @extend_schema(responses={200: common.serializers.TaskDetailSerializer})
    def get(self, request, task_id, *args, **kwargs):
        """Fetch information regarding a particular background task ID."""
        response = common.serializers.TaskDetailSerializer.from_task(task_id).data

        return Response(response, status=response['http_status'])


class BackgroundTaskOverview(APIView):
    """Provides an overview of the background task queue status."""

    permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
    serializer_class = None

    @extend_schema(
        operation_id='background_task_overview',
        responses={200: common.serializers.TaskOverviewSerializer},
    )
    def get(self, request, fmt=None):
        """Return information about the current status of the background task queue."""
        import django_q.models as q_models

        import InvenTree.status

        serializer = common.serializers.TaskOverviewSerializer({
            'is_running': InvenTree.status.is_worker_running(),
            'pending_tasks': q_models.OrmQ.objects.count(),
            'scheduled_tasks': q_models.Schedule.objects.count(),
            'failed_tasks': q_models.Failure.objects.count(),
        })

        return Response(serializer.data)


class PendingTaskList(BulkDeleteMixin, ListAPI):
    """Provides a read-only list of currently pending tasks."""

    permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]

    queryset = django_q.models.OrmQ.objects.all()
    serializer_class = common.serializers.PendingTaskSerializer


class ScheduledTaskList(ListAPI):
    """Provides a read-only list of currently scheduled tasks."""

    permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]

    queryset = django_q.models.Schedule.objects.all()
    serializer_class = common.serializers.ScheduledTaskSerializer

    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['pk', 'func', 'last_run', 'next_run']

    search_fields = ['func', 'name']

    def get_queryset(self):
        """Return annotated queryset."""
        queryset = super().get_queryset()
        return common.serializers.ScheduledTaskSerializer.annotate_queryset(queryset)


class FailedTaskList(BulkDeleteMixin, ListAPI):
    """Provides a read-only list of currently failed tasks."""

    permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]

    queryset = django_q.models.Failure.objects.all()
    serializer_class = common.serializers.FailedTaskSerializer

    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['pk', 'func', 'started', 'stopped']

    search_fields = ['func']


class FlagList(ListAPI):
    """List view for feature flags."""

    queryset = settings.FLAGS
    serializer_class = common.serializers.FlagSerializer
    permission_classes = [AllowAnyOrReadScope]

    # Specifically disable pagination for this view
    pagination_class = None


class FlagDetail(RetrieveAPI):
    """Detail view for an individual feature flag."""

    serializer_class = common.serializers.FlagSerializer
    permission_classes = [AllowAnyOrReadScope]

    def get_object(self):
        """Attempt to find a config object with the provided key."""
        key = self.kwargs['key']
        value = settings.FLAGS.get(key, None)
        if not value:
            raise NotFound()
        return {key: value}


class ContentTypeList(ListAPI):
    """List view for ContentTypes."""

    queryset = ContentType.objects.all()
    serializer_class = common.serializers.ContentTypeSerializer
    permission_classes = [IsAuthenticatedOrReadScope]
    filter_backends = SEARCH_ORDER_FILTER
    search_fields = ['app_label', 'model']


class ContentTypeDetail(RetrieveAPI):
    """Detail view for a ContentType model."""

    queryset = ContentType.objects.all()
    serializer_class = common.serializers.ContentTypeSerializer
    permission_classes = [IsAuthenticatedOrReadScope]


class ContentTypeModelDetail(ContentTypeDetail):
    """Detail view for a ContentType model."""

    def get_object(self):
        """Attempt to find a ContentType object with the provided key."""
        model_ref = self.kwargs.get('model', None)
        if model_ref:
            qs = self.filter_queryset(self.get_queryset())
            try:
                return qs.get(model=model_ref)
            except ContentType.DoesNotExist:
                raise NotFound()
        raise NotFound()

    @extend_schema(operation_id='contenttype_retrieve_model')
    def get(self, request, *args, **kwargs):
        """Detail view for a ContentType model."""
        return super().get(request, *args, **kwargs)


class AttachmentFilter(FilterSet):
    """Filterset for the AttachmentList API endpoint."""

    class Meta:
        """Metaclass options."""

        model = common.models.Attachment
        fields = ['model_type', 'model_id', 'upload_user', 'is_image']

    has_thumbnail = rest_filters.BooleanFilter(
        label=_('Has Thumbnail'), method='filter_has_thumbnail'
    )

    def filter_has_thumbnail(self, queryset, name, value):
        """Filter attachments based on whether they have a thumbnail or not."""
        if value:
            return queryset.exclude(thumbnail=None).exclude(thumbnail='')
        return queryset.filter(Q(thumbnail=None) | Q(thumbnail='')).distinct()

    is_link = rest_filters.BooleanFilter(label=_('Is Link'), method='filter_is_link')

    def filter_is_link(self, queryset, name, value):
        """Filter attachments based on whether they are a link or not."""
        if value:
            return queryset.exclude(link=None).exclude(link='')
        return queryset.filter(Q(link=None) | Q(link='')).distinct()

    is_file = rest_filters.BooleanFilter(label=_('Is File'), method='filter_is_file')

    def filter_is_file(self, queryset, name, value):
        """Filter attachments based on whether they are a file or not."""
        if value:
            return queryset.exclude(attachment=None).exclude(attachment='')
        return queryset.filter(Q(attachment=None) | Q(attachment='')).distinct()

    tags = common.filters.TagsFilter()


class AttachmentMixin:
    """Mixin class for Attachment views."""

    queryset = common.models.Attachment.objects.all()
    serializer_class = common.serializers.AttachmentSerializer
    permission_classes = [IsAuthenticatedOrReadScope]


class AttachmentList(AttachmentMixin, BulkDeleteMixin, ListCreateAPI):
    """List API endpoint for Attachment objects."""

    filter_backends = SEARCH_ORDER_FILTER
    filterset_class = AttachmentFilter

    ordering_fields = ['model_id', 'model_type', 'upload_date', 'file_size']
    search_fields = ['comment', 'model_id', 'model_type']

    def perform_create(self, serializer):
        """Save the user information when a file is uploaded."""
        serializer.save(upload_user=self.request.user)

    def validate_delete(self, queryset, request) -> None:
        """Ensure that the user has correct permissions for a bulk-delete.

        - Extract all model types from the provided queryset
        - Ensure that the user has correct 'delete' permissions for each model
        """
        from common.validators import attachment_model_class_from_label
        from users.permissions import check_user_permission

        model_types = queryset.values_list('model_type', flat=True).distinct()

        for model_type in model_types:
            if model_class := attachment_model_class_from_label(model_type):
                if not check_user_permission(request.user, model_class, 'delete'):
                    raise ValidationError(
                        _('User does not have permission to delete these attachments')
                    )


class AttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
    """Detail API endpoint for Attachment objects."""

    def update(self, request, *args, **kwargs):
        """Update an existing attachment object."""
        attachment = self.get_object()

        if not attachment.check_permission('change', request.user):
            raise PermissionDenied(
                _('User does not have permission to edit this attachment')
            )

        partial = kwargs.pop('partial', False)
        data = self.clean_data(request.data)

        # Extract filename first
        filename = data.pop('filename', None)

        # Run other validation / updates first, before attempting to rename the file
        serializer = self.get_serializer(attachment, data=data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        # User is attempting to rename the file
        if filename and attachment.basename and filename != attachment.basename:
            attachment.rename(filename)

        return Response(serializer.data)

    def destroy(self, request, *args, **kwargs):
        """Delete an existing attachment object."""
        attachment = self.get_object()

        if not attachment.check_permission('delete', request.user):
            raise PermissionDenied(
                _('User does not have permission to delete this attachment')
            )

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


class NoteFilter(FilterSet):
    """Filterset class for the NoteList API endpoint."""

    class Meta:
        """Metaclass options for the filterset."""

        model = common.models.Note
        fields = ['model_type', 'model_id', 'updated_by', 'template']

    template = rest_filters.BooleanFilter(label='Template')

    model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')

    def filter_model_type(self, queryset, name, value):
        """Filter queryset by model type, allowing null for global templates."""
        return common.filters.filter_content_type(
            queryset, 'model_type', value, allow_null=True
        )


class NoteMixin:
    """Mixin class for the Note views."""

    # Ignore default sanitizing of the 'content' field
    # Note: This is handled explicitly in the 'save' method of the Note model
    SAFE_FIELDS = ['content']

    queryset = common.models.Note.objects.all()
    serializer_class = common.serializers.NoteSerializer
    permission_classes = [IsAuthenticatedOrReadScope]

    def get_queryset(self):
        """Filter notes to those the requesting user has view permission for.

        Template notes (no attached model) are always visible.
        Regular notes are only visible when the user has 'view' permission
        for the model type the note is linked to.
        """
        from InvenTree.models import InvenTreeNoteMixin
        from users.permissions import check_user_permission

        qs = super().get_queryset()
        user = self.request.user

        if user.is_superuser:
            return qs

        allowed_ct_ids = []
        for ct in ContentType.objects.all():
            model_class = ct.model_class()
            if model_class and issubclass(model_class, InvenTreeNoteMixin):
                if check_user_permission(user, model_class, 'view'):
                    allowed_ct_ids.append(ct.pk)

        return qs.filter(Q(template=True) | Q(model_type__in=allowed_ct_ids))


class NoteList(NoteMixin, ListCreateAPI):
    """List API endpoint for Note objects."""

    filter_backends = SEARCH_ORDER_FILTER
    filterset_class = NoteFilter

    ordering = '-primary'
    ordering_fields = [
        'model_id',
        'model_type',
        'updated_by',
        'updated',
        'primary',
        'template',
        'title',
    ]
    search_fields = ['title', 'description', 'content']


class NoteDetail(NoteMixin, RetrieveUpdateDestroyAPI):
    """Detail API endpoint for Note objects."""


class ParameterTemplateFilter(FilterSet):
    """FilterSet class for the ParameterTemplateList API endpoint."""

    class Meta:
        """Metaclass options."""

        model = common.models.ParameterTemplate
        fields = ['name', 'units', 'checkbox', 'enabled']

    has_choices = rest_filters.BooleanFilter(
        method='filter_has_choices', label='Has Choice'
    )

    def filter_has_choices(self, queryset, name, value):
        """Filter queryset to include only PartParameterTemplates with choices."""
        if str2bool(value):
            return queryset.exclude(Q(choices=None) | Q(choices=''))

        return queryset.filter(Q(choices=None) | Q(choices='')).distinct()

    has_units = rest_filters.BooleanFilter(method='filter_has_units', label='Has Units')

    def filter_has_units(self, queryset, name, value):
        """Filter queryset to include only PartParameterTemplates with units."""
        if str2bool(value):
            return queryset.exclude(Q(units=None) | Q(units=''))

        return queryset.filter(Q(units=None) | Q(units='')).distinct()

    model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')

    def filter_model_type(self, queryset, name, value):
        """Filter queryset to include only ParameterTemplates of the given model type."""
        return common.filters.filter_content_type(
            queryset, 'model_type', value, allow_null=False
        )

    for_model = rest_filters.CharFilter(method='filter_for_model', label='For Model')

    def filter_for_model(self, queryset, name, value):
        """Filter queryset to include only ParameterTemplates which apply to the given model.

        Note that this varies from the 'model_type' filter, in that ParameterTemplates
        with a blank 'model_type' are considered to apply to all models.
        """
        return common.filters.filter_content_type(
            queryset, 'model_type', value, allow_null=True
        )

    exists_for_model = rest_filters.CharFilter(
        method='filter_exists_for_model', label='Exists For Model'
    )

    def filter_exists_for_model(self, queryset, name, value):
        """Filter queryset to include only ParameterTemplates which have at least one Parameter for the given model type."""
        content_type = common.filters.determine_content_type(value)

        if not content_type:
            raise ValidationError({
                'exists_for_model': 'Invalid model type provided - unable to determine content type'
            })

        # If the 'filter_exists_for_model_id' filter is applied, defer to that
        if self.request.query_params.get('exists_for_model_id', None):
            return queryset

        queryset = queryset.prefetch_related('parameters')

        # Annotate the queryset to determine which ParameterTemplates have at least one Parameter for the given model type
        queryset = queryset.annotate(
            parameter_count=SubqueryCount(
                'parameters', filter=Q(model_type=content_type)
            )
        )

        # Return only those ParameterTemplates which have at least one Parameter for the given model type
        return queryset.filter(parameter_count__gt=0)

    exists_for_model_id = rest_filters.NumberFilter(
        method='filter_exists_for_model_id', label='Exists For Model ID'
    )

    def filter_exists_for_model_id(self, queryset, name, value):
        """Filter queryset to include only ParameterTemplates which have at least one Parameter for the given model type and model id.

        Notes:
            - This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a particular model type.

        Reference: https://github.com/inventree/InvenTree/issues/11381
        """
        exists_for_model = self.request.query_params.get('exists_for_model', None)

        if not exists_for_model:
            raise ValidationError({
                'exists_for_model': 'Invalid model type provided - unable to determine content type'
            })

        content_type = common.filters.determine_content_type(exists_for_model)

        if not content_type:
            raise ValidationError({
                'exists_for_model': 'Invalid model type provided - unable to determine content type'
            })

        model_class = content_type.model_class()

        # Try to find the model instance
        try:
            instance = model_class.objects.get(pk=value)
        except (model_class.DoesNotExist, ValueError):
            # If the model instance does not exist, then we can return an empty queryset
            raise ValidationError({
                'exists_for_model_id': 'Invalid model id provided - no such instance for the given model type'
            })

        # If the provided model is a "tree" structure, then we should also include any child objects in the filter
        if isinstance(instance, InvenTree.models.InvenTreeTree):
            id_values = list(
                instance.get_descendants(include_self=True).values_list('pk', flat=True)
            )
        else:
            id_values = [instance.pk]

        # Now, filter against model type and model id
        queryset = queryset.prefetch_related('parameters')

        filters = {'model_type': content_type, 'model_id__in': id_values}

        # Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined
        queryset = queryset.annotate(
            parameter_count=SubqueryCount('parameters', filter=Q(**filters))
        )

        return queryset.filter(parameter_count__gt=0)

    exists_for_related_model = rest_filters.CharFilter(
        method='filter_exists_for_related_model', label='Exists For Related Model'
    )

    def filter_exists_for_related_model(self, queryset, name, value):
        """Filter applied to map parameter templates to a particular model relation against the target model.

        For instance, specify 'category' to filter part parameters which exist for any part in that category.

        Note:
            - This filter has no effect on its own
            - It requires the 'exists_for_model' filter to be applied (to specify the base model)
            - It requires the 'exists_for_related_model_id' filter to be applied also (to specify the related model id)
        """
        return queryset

    exists_for_related_model_id = rest_filters.NumberFilter(
        method='filter_exists_for_related_model_id', label='Exists For Model ID'
    )

    def filter_exists_for_related_model_id(self, queryset, name, value):
        """Filter queryset to include only ParameterTemplates which have at least one Parameter for the given related model type and model id.

        Notes:
            - This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a base model
            - This filter can only be applied if the 'exists_for_related_model' filter is also applied, as the related model id is only meaningful in the context of a particular model relation

        Example: To filter part parameters which have at least one parameter defined for any part in category 5, you could apply the following filters:
            - exists_for_model=part
            - exists_for_related_model=category
            - exists_for_related_model_id=5
        """
        model = self.request.query_params.get('exists_for_model', None)
        related_model = self.request.query_params.get('exists_for_related_model', None)

        if not model or not related_model:
            raise ValidationError({
                'exists_for_model': 'Invalid model type provided - unable to determine content type'
            })

        # Determine content type for the base model, to ensure they are valid
        model_type = common.filters.determine_content_type(model)

        if not model_type:
            return queryset.none()

        # Determine the model class for the 'related' model
        try:
            related_model_field = model_type.model_class()._meta.get_field(
                related_model
            )
        except FieldDoesNotExist:
            raise ValidationError({
                'exists_for_related_model': 'Invalid related model - no such field on the base model'
            })
        if related_model_field := model_type.model_class()._meta.get_field(
            related_model
        ):
            related_model_class = related_model_field.related_model
        else:
            # Return an empty queryset if the provided related model is invalid
            return queryset.none()

        # Find all instances of the related model which match the provided related model id
        try:
            related_instance = related_model_class.objects.get(pk=value)
        except (related_model_class.DoesNotExist, ValueError):
            return queryset.none()

        # Account for potential tree structure in the related model
        if isinstance(related_instance, InvenTree.models.InvenTreeTree):
            related_instances = list(
                related_instance.get_descendants(include_self=True).values_list(
                    'pk', flat=True
                )
            )
        else:
            related_instances = [related_instance.pk]

        # Next, find all instances of the base model which are related to the related model instances
        model_instances = model_type.model_class().objects.filter(**{
            f'{related_model}__in': related_instances
        })
        model_instance_ids = list(model_instances.values_list('pk', flat=True))

        # Now, filter against model type and model id
        queryset = queryset.prefetch_related('parameters')

        filters = {'model_type': model_type, 'model_id__in': model_instance_ids}

        # Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined
        queryset = queryset.annotate(
            parameter_count=SubqueryCount('parameters', filter=Q(**filters))
        )

        return queryset.filter(parameter_count__gt=0)


class ParameterTemplateMixin:
    """Mixin class for ParameterTemplate views."""

    queryset = common.models.ParameterTemplate.objects.all().prefetch_related(
        'model_type'
    )
    serializer_class = common.serializers.ParameterTemplateSerializer
    permission_classes = [IsAuthenticatedOrReadScope]


class ParameterTemplateList(ParameterTemplateMixin, DataExportViewMixin, ListCreateAPI):
    """List view for ParameterTemplate objects."""

    filterset_class = ParameterTemplateFilter
    filter_backends = SEARCH_ORDER_FILTER
    search_fields = ['name', 'description']
    ordering_fields = ['name', 'units', 'checkbox']


class ParameterTemplateDetail(ParameterTemplateMixin, RetrieveUpdateDestroyAPI):
    """Detail view for a ParameterTemplate object."""


class ParameterFilter(FilterSet):
    """Custom filters for the ParameterList API endpoint."""

    class Meta:
        """Metaclass options for the filterset."""

        model = common.models.Parameter
        fields = ['model_id', 'template', 'updated_by']

    enabled = rest_filters.BooleanFilter(
        label='Template Enabled', field_name='template__enabled'
    )

    model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')

    def filter_model_type(self, queryset, name, value):
        """Filter queryset to include only Parameters of the given model type."""
        return common.filters.filter_content_type(
            queryset, 'model_type', value, allow_null=False
        )


class ParameterMixin:
    """Mixin class for Parameter views."""

    queryset = common.models.Parameter.objects.all().prefetch_related('model_type')
    serializer_class = common.serializers.ParameterSerializer
    permission_classes = [IsAuthenticatedOrReadScope]


class ParameterList(
    OutputOptionsMixin,
    ParameterMixin,
    BulkCreateMixin,
    BulkDeleteMixin,
    DataExportViewMixin,
    ListCreateAPI,
):
    """List API endpoint for Parameter objects."""

    filterset_class = ParameterFilter
    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['name', 'data', 'units', 'template', 'updated', 'updated_by']

    ordering_field_aliases = {
        'name': 'template__name',
        'units': 'template__units',
        'data': ['data_numeric', 'data'],
    }

    search_fields = [
        'data',
        'template__name',
        'template__description',
        'template__units',
    ]

    unique_create_fields = ['model_type', 'model_id', 'template']


class ParameterDetail(ParameterMixin, RetrieveUpdateDestroyAPI):
    """Detail API endpoint for Parameter objects."""


@method_decorator(cache_control(public=True, max_age=86400), name='dispatch')
class IconList(ListAPI):
    """List view for available icon packages."""

    serializer_class = common.serializers.IconPackageSerializer
    permission_classes = [AllowAnyOrReadScope]

    def get_queryset(self):
        """Return a list of all available icon packages."""
        return list(get_icon_packs().values())


class SelectionListMixin(OutputOptionsMixin):
    """Mixin for SelectionList views."""

    queryset = common.models.SelectionList.objects.all()
    serializer_class = common.serializers.SelectionListSerializer
    permission_classes = [IsAuthenticatedOrReadScope]

    def get_queryset(self):
        """Override the queryset method to include entry count."""
        return self.serializer_class.annotate_queryset(super().get_queryset())


class SelectionListList(SelectionListMixin, ListCreateAPI):
    """List view for SelectionList objects."""


class SelectionListDetail(SelectionListMixin, RetrieveUpdateDestroyAPI):
    """Detail view for a SelectionList object."""


class EntryMixin:
    """Mixin for SelectionEntry views."""

    queryset = common.models.SelectionListEntry.objects.all()
    serializer_class = common.serializers.SelectionEntrySerializer
    permission_classes = [IsAuthenticatedOrReadScope]
    lookup_url_kwarg = 'entrypk'

    def get_queryset(self):
        """Prefetch related fields."""
        pk = self.kwargs.get('pk', None)
        queryset = super().get_queryset().filter(list=pk)
        queryset = queryset.prefetch_related('list')
        return queryset

    def perform_destroy(self, instance):
        """Prevent deletion of entries belonging to a locked selection list."""
        if instance.list.locked:
            raise PermissionDenied(_('Selection list is locked'))
        super().perform_destroy(instance)


class SelectionEntryList(EntryMixin, ListCreateAPI):
    """List view for SelectionEntry objects."""

    filter_backends = SEARCH_ORDER_FILTER

    ordering_fields = ['list', 'label', 'active']

    search_fields = ['label', 'description']

    filterset_fields = ['active', 'value', 'list']


class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
    """Detail view for a SelectionEntry object."""


class DataOutputViewSet(BulkDeleteViewsetMixin, RetrieveDestroyModelViewSet):
    """Mixin class for DataOutput endpoints."""

    queryset = common.models.DataOutput.objects.all()
    serializer_class = common.serializers.DataOutputSerializer
    permission_classes = [IsAuthenticatedOrReadScope]

    filter_backends = SEARCH_ORDER_FILTER
    ordering_fields = ['pk', 'user', 'plugin', 'output_type', 'created']
    filterset_fields = ['user']

    def get_queryset(self):
        """Return the set of DataOutput objects which the user has permission to view."""
        queryset = super().get_queryset()

        try:
            user = self.request.user
        except AttributeError:
            raise PermissionDenied('User information is not available')

        # Allow staff users access to all DataOutput objects
        if user.is_staff:
            return queryset

        # All other users are limited to viewing their own DataOutput objects
        return queryset.filter(user=user)


common_router.register('data-output', DataOutputViewSet, basename='api-data-output')


class EmailViewSet(BulkDeleteViewsetMixin, RetrieveDestroyModelViewSet):
    """Backend E-Mail management for administrative purposes."""

    queryset = common.models.EmailMessage.objects.all()
    serializer_class = common.serializers.EmailMessageSerializer
    permission_classes = [IsSuperuserOrSuperScope]

    filter_backends = SEARCH_ORDER_FILTER
    ordering_fields = [
        'created',
        'subject',
        'to',
        'sender',
        'status',
        'timestamp',
        'direction',
    ]
    search_fields = [
        'subject',
        'to',
        'sender',
        'global_id',
        'message_id_key',
        'thread_id_key',
    ]

    @action(
        detail=False,
        methods=['post'],
        serializer_class=common.serializers.TestEmailSerializer,
    )
    def test(self, request):
        """Send a test email."""
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        data = serializer.validated_data

        delivered, reason = send_email(
            subject='Test email from InvenTree',
            body='This is a test email from InvenTree.',
            recipients=[data['email']],
        )
        if not delivered:
            raise serializers.ValidationError(
                detail=f'Failed to send test email: "{reason}"'
            )  # pragma: no cover
        return Response(serializer.data)


admin_router.register('email', EmailViewSet, basename='api-email')


class HealthCheckStatusSerializer(serializers.Serializer):
    """Status of the overall system health."""

    status = serializers.ChoiceField(
        help_text='Health status of the InvenTree server',
        choices=['ok', 'loading'],
        read_only=True,
        default='ok',
    )


class HealthCheckView(APIView):
    """Simple JSON endpoint for InvenTree health check.

    Intended to be used by external services to confirm that the InvenTree server is running.
    """

    permission_classes = [AllowAnyOrReadScope]

    @extend_schema(
        responses={
            200: OpenApiResponse(
                response=HealthCheckStatusSerializer,
                description='InvenTree server health status',
            )
        }
    )
    def get(self, request, *args, **kwargs):
        """Simple health check endpoint for monitoring purposes.

        Use the root API endpoint for more detailed information (using an authenticated request).
        """
        status = (
            InvenTree.ready.isPluginRegistryLoaded()
            if settings.PLUGINS_ENABLED
            else True
        )
        return JsonResponse(
            {'status': 'ok' if status else 'loading'}, status=200 if status else 503
        )


class ObservabilityEndSerializer(serializers.Serializer):
    """Serializer for observability end endpoint."""

    traceid = serializers.CharField(
        help_text='Trace ID to end', max_length=128, required=True
    )
    service = serializers.CharField(
        help_text='Service name', max_length=128, required=True
    )


class ObservabilityEnd(CreateAPI):
    """Endpoint for observability tools."""

    # Note: This endpoint can be called anonymously, as it needs to function before the user is authenticated (e.g. during login)
    permission_classes = [AllowAnyOrReadScope]
    serializer_class = ObservabilityEndSerializer

    def create(self, request, *args, **kwargs):
        """End a trace in the observability system."""
        if not settings.TRACING_ENABLED:
            return Response({'status': 'ok'})

        data = self.get_serializer(data=request.data)
        data.is_valid(raise_exception=True)

        traceid = data.validated_data['traceid']
        # service = data.validated_data['service']  # This will become interesting with frontend observability

        # End the foreign trend via the low level otel API
        tracer = trace.get_tracer(__name__)
        span_context = trace.SpanContext(
            trace_id=int(traceid, 16),
            span_id=0,
            is_remote=True,
            trace_flags=trace.TraceFlags(0x01),
            trace_state=trace.TraceState(),
        )
        with tracer.start_span('Ending session') as span:
            span.add_event('Ending external trace')
            span.add_link(span_context)

        return Response({'status': 'ok'})


selection_urls = [
    path(
        '<int:pk>/',
        include([
            # Entries
            path(
                'entry/',
                include([
                    path(
                        '<int:entrypk>/',
                        include([
                            path(
                                '',
                                SelectionEntryDetail.as_view(),
                                name='api-selectionlistentry-detail',
                            )
                        ]),
                    ),
                    path(
                        '',
                        SelectionEntryList.as_view(),
                        name='api-selectionlistentry-list',
                    ),
                ]),
            ),
            path('', SelectionListDetail.as_view(), name='api-selectionlist-detail'),
        ]),
    ),
    path('', SelectionListList.as_view(), name='api-selectionlist-list'),
]

# API URL patterns
settings_api_urls = [
    # User settings
    path(
        'user/',
        include([
            # User Settings Detail
            re_path(
                r'^(?P<key>\w+)/',
                UserSettingsDetail.as_view(),
                name='api-user-setting-detail',
            ),
            # User Settings List
            path('', UserSettingsList.as_view(), name='api-user-setting-list'),
        ]),
    ),
    # Global settings
    path(
        'global/',
        include([
            # Global Settings Detail
            re_path(
                r'^(?P<key>\w+)/',
                GlobalSettingsDetail.as_view(),
                name='api-global-setting-detail',
            ),
            # Global Settings List
            path('', GlobalSettingsList.as_view(), name='api-global-setting-list'),
        ]),
    ),
]

common_api_urls = [
    # Webhooks
    path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
    # Background task information
    path(
        'background-task/',
        include([
            path('pending/', PendingTaskList.as_view(), name='api-pending-task-list'),
            path(
                'scheduled/',
                ScheduledTaskList.as_view(),
                name='api-scheduled-task-list',
            ),
            path('failed/', FailedTaskList.as_view(), name='api-failed-task-list'),
            path(
                '<str:task_id>/', BackgroundTaskDetail.as_view(), name='api-task-detail'
            ),
            path('', BackgroundTaskOverview.as_view(), name='api-task-overview'),
        ]),
    ),
    # Attachments
    path(
        'attachment/',
        include([
            path(
                '<int:pk>/',
                include([
                    meta_path(common.models.Attachment),
                    path('', AttachmentDetail.as_view(), name='api-attachment-detail'),
                ]),
            ),
            path('', AttachmentList.as_view(), name='api-attachment-list'),
        ]),
    ),
    # Notes
    path(
        'note/',
        include([
            # Uploaded images for notes
            path('image/', NotesImageList.as_view(), name='api-notes-image-list'),
            path(
                '<int:pk>/',
                include([
                    meta_path(common.models.Note),
                    path('', NoteDetail.as_view(), name='api-note-detail'),
                ]),
            ),
            path('', NoteList.as_view(), name='api-note-list'),
        ]),
    ),
    # Parameters and templates
    path(
        'parameter/',
        include([
            path(
                'template/',
                include([
                    path(
                        '<int:pk>/',
                        include([
                            meta_path(common.models.ParameterTemplate),
                            path(
                                '',
                                ParameterTemplateDetail.as_view(),
                                name='api-parameter-template-detail',
                            ),
                        ]),
                    ),
                    path(
                        '',
                        ParameterTemplateList.as_view(),
                        name='api-parameter-template-list',
                    ),
                ]),
            ),
            path(
                '<int:pk>/',
                include([
                    meta_path(common.models.Parameter),
                    path('', ParameterDetail.as_view(), name='api-parameter-detail'),
                ]),
            ),
            path('', ParameterList.as_view(), name='api-parameter-list'),
        ]),
    ),
    # Metadata
    path(
        'metadata/',
        include([
            path(
                '<str:model>/<str:lookup_field>/<str:lookup_value>/',
                GenericMetadataView.as_view(),
                name='api-generic-metadata',
            ),
            path(
                '<str:model>/<int:pk>/',
                SimpleGenericMetadataView.as_view(),
                name='api-generic-metadata',
            ),
        ]),
    ),
    # Project codes
    path(
        'project-code/',
        include([
            path(
                '<int:pk>/',
                include([
                    meta_path(common.models.ProjectCode),
                    path(
                        '', ProjectCodeDetail.as_view(), name='api-project-code-detail'
                    ),
                ]),
            ),
            path('', ProjectCodeList.as_view(), name='api-project-code-list'),
        ]),
    ),
    # Tags (via django-taggit)
    path(
        'tag/',
        include([
            path('<int:pk>/', TagDetail.as_view(), name='api-tag-detail'),
            path('', TagList.as_view(), name='api-tag-list'),
        ]),
    ),
    # Flags
    path(
        'flags/',
        include([
            path('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
            path('', FlagList.as_view(), name='api-flag-list'),
        ]),
    ),
    # Status
    path('generic/status/', include(generic_states_api_urls)),
    # Contenttype
    path(
        'contenttype/',
        include([
            path(
                '<int:pk>/', ContentTypeDetail.as_view(), name='api-contenttype-detail'
            ),
            path(
                'model/<str:model>/',
                ContentTypeModelDetail.as_view(),
                name='api-contenttype-detail-modelname',
            ),
            path('', ContentTypeList.as_view(), name='api-contenttype-list'),
        ]),
    ),
    # Icons
    path('icons/', IconList.as_view(), name='api-icon-list'),
    # Selection lists
    path('selection/', include(selection_urls)),
    # System APIs (related to basic system functions)
    path(
        'system/',
        include([
            # Health check
            path('health/', HealthCheckView.as_view(), name='api-system-health')
        ]),
    ),
    # Internal System APIs - DO NOT USE
    path(
        'system-internal/',
        include([
            # Observability
            path(
                'observability/end',
                ObservabilityEnd.as_view(),
                name='api-system-observability',
            )
        ]),
    ),
    # Router
    path('', include(common_router.urls)),
]

admin_api_urls = admin_router.urls
