"""SatNOGS DB API django rest framework Views"""
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.db import IntegrityError
from django.db.models import F
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema, \
    extend_schema_view
from rest_framework import mixins, status, viewsets
from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.serializers import ValidationError

from db.api import filters, pagination, serializers
from db.api.parsers import JSONLDParser
from db.api.perms import IsAuthenticatedOrOptions, SafeMethodsWithPermission
from db.api.renderers import BrowsableAPIRendererWithoutForms, BrowserableJSONLDRenderer, \
    JSONLDRenderer, TLERenderer
from db.api.serializers import OpticalObservationCreateSerializer, OpticalObservationSerializer
from db.api.throttling import GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle, \
    GetTelemetryViolatorThrottle
from db.base.helpers import gridsquare
from db.base.models import SATELLITE_STATUS, SERVICE_TYPE, TRANSMITTER_STATUS, TRANSMITTER_TYPE, \
    Artifact, DemodData, LatestTleSet, Mode, OpticalObservation, Satellite, SatelliteEntry, \
    SatelliteIdentifier, Transmitter, TransmitterEntry
from db.base.tasks import decode_current_frame, publish_current_frame, update_satellite_name

ISS_EXAMPLE = OpenApiExample('25544 (ISS)', value=25544)


class CachedListModelMixin(mixins.ListModelMixin):  # pylint: disable=R0903
    """A class that caches its list response if no request parameters are given"""
    cache_key = None

    def list(self, request, *args, **kwargs):
        assert self.cache_key or hasattr(self, "get_cache_key"), "cache_key field not set in class"

        if any(request.query_params.values()) or self.paginator:
            return super().list(request, *args, **kwargs)

        cached_data = cache.get(self.cache_key or self.get_cache_key(request))
        if cached_data:
            return Response(cached_data)

        queryset = self.filter_queryset(self.get_queryset())

        serializer = self.get_serializer(queryset, many=True)
        data = serializer.data

        cache.set(
            self.cache_key or self.get_cache_key(request), data,
            settings.CACHE_INVALIDATION_TIMEOUT
        )
        return Response(data)


@extend_schema_view(
    retrieve=extend_schema(
        description='Retrieve a single RF Mode from SatNOGS DB based on its ID',
    ),
    list=extend_schema(description='Retrieve a complete list of RF Modes from SatNOGS DB', )
)
class ModeViewSet(CachedListModelMixin, viewsets.ReadOnlyModelViewSet):  # pylint: disable=R0901
    """
    Read-only view into the transmitter modulation modes (RF Modes) currently tracked
    in the SatNOGS DB database

    For more details on individual RF mode types please [see our wiki][moderef].

    [moderef]: https://wiki.satnogs.org/Category:RF_Modes
    """
    renderer_classes = [
        JSONRenderer, BrowsableAPIRendererWithoutForms, JSONLDRenderer, BrowserableJSONLDRenderer
    ]
    queryset = Mode.objects.all()
    serializer_class = serializers.ModeSerializer
    cache_key = Mode.CacheOptions.default_cache_key


@extend_schema_view(
    list=extend_schema(
        description='Retrieve a full or filtered list of satellites in SatNOGS DB',
        parameters=[
            # drf-spectacular does not currently recognize the in_orbit filter as a
            # bool, forcing it here. See drf-spectacular#234
            OpenApiParameter(
                name='in_orbit',
                description='Filter by satellites currently in orbit (True) or those that have \
                            decayed (False)',
                required=False,
                type=bool
            ),
            OpenApiParameter(
                name='status',
                description='Filter by satellite status: ' + ' '.join(SATELLITE_STATUS),
                required=False,
                type=OpenApiTypes.STR
            ),
            OpenApiParameter(
                name='norad_cat_id',
                description='Select a satellite by its NORAD-assigned identifier',
                examples=[ISS_EXAMPLE],
            ),
        ],
    ),
    retrieve=extend_schema(
        description='Retrieve details on a single satellite in SatNOGS DB',
        parameters=[
            OpenApiParameter(
                'satellite_identifier__sat_id',
                OpenApiTypes.STR,
                OpenApiParameter.PATH,
                description='Select a satellite by its Satellite Identifier',
            ),
        ],
    ),
)
class SatelliteViewSet(  # pylint: disable=R0901
        CachedListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
        viewsets.GenericViewSet):
    """
    View into the Satellite entities in the SatNOGS DB database
    """
    renderer_classes = [
        JSONRenderer, BrowsableAPIRendererWithoutForms, JSONLDRenderer, BrowserableJSONLDRenderer
    ]
    parser_classes = [JSONLDParser]
    queryset = Satellite.objects.filter(
        associated_satellite__isnull=True, satellite_entry__approved=True
    ).prefetch_related('associated_with', 'telemetries')
    serializer_class = serializers.SatelliteSerializer
    filterset_class = filters.SatelliteViewFilter
    lookup_field = 'satellite_identifier__sat_id'
    cache_key = Satellite.CacheOptions.default_cache_key

    def get_object(self):
        queryset = self.get_queryset()
        # Apply any filter backends
        queryset = self.filter_queryset(queryset)

        # In case user uses NORAD ID for getting satellite
        if 'satellite_entry__norad_cat_id' in self.kwargs:
            norad_cat_id = self.kwargs['satellite_entry__norad_cat_id']
            return get_object_or_404(queryset, satellite_entry__norad_cat_id=norad_cat_id)

        # Getting satellite by using Satellite Identifier
        sat_id = self.kwargs['satellite_identifier__sat_id']
        try:
            return queryset.get(satellite_identifier__sat_id=sat_id)
        except Satellite.DoesNotExist:
            return get_object_or_404(
                queryset, associated_with__satellite_identifier__sat_id=sat_id
            )

    def create(self, request, *args, **kwargs):  # noqa: C901; pylint: disable=R0911,R0912,R0915
        """
        Creates a satellite suggestion.
        """
        satellites_data = []
        for satellite_entry in request.data['@graph']:
            if 'satellite' not in satellite_entry:
                data = 'Satellite Entry without "satellite" key'
                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            if not satellite_entry['satellite']:
                data = 'One or more of the required fields are missing.\n Required fields: \
                        name, status, citation'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            satellite = satellite_entry['satellite']
            create_satellite_identifier = False

            satellite_data = {}
            if "@id" not in satellite:
                data = 'Missing "@id" for one or more entries'
                return Response(data, status=status.HTTP_400_BAD_REQUEST)

            if 'sat_id' in satellite:
                if isinstance(satellite['sat_id'], list):
                    data = 'Multiple values for "http://schema.org/identifier" or multiple \
                            entries with the same "@id" and different \
                            "http://schema.org/identifier" values'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                sat_id = satellite['sat_id']
                try:
                    satellite_object = Satellite.objects.get(satellite_identifier__sat_id=sat_id)
                    satellite_data['satellite_identifier'
                                   ] = satellite_object.satellite_entry.satellite_identifier.pk
                    satellite_data['norad_follow_id'
                                   ] = satellite_object.satellite_entry.norad_follow_id
                    satellite_data['description'] = satellite_object.satellite_entry.description
                    satellite_data['dashboard_url'
                                   ] = satellite_object.satellite_entry.dashboard_url
                    satellite_data['image'] = satellite_object.satellite_entry.image
                    satellite_data['decayed'] = satellite_object.satellite_entry.decayed
                    satellite_data['countries'] = satellite_object.satellite_entry.countries
                    satellite_data['website'] = satellite_object.satellite_entry.website
                    satellite_data['launched'] = satellite_object.satellite_entry.launched
                    satellite_data['deployed'] = satellite_object.satellite_entry.deployed
                    satellite_data['operator'] = satellite_object.satellite_entry.operator
                except Satellite.DoesNotExist:
                    try:
                        satellite_identifier = SatelliteIdentifier.objects.get(sat_id=sat_id)
                        satellite_data['satellite_identifier'] = satellite_identifier.pk
                    except SatelliteIdentifier.DoesNotExist:
                        satellite_identifier = SatelliteIdentifier.objects.create(sat_id=sat_id)
                        satellite_data['satellite_identifier'] = satellite_identifier.pk

            else:
                create_satellite_identifier = True

            if isinstance(satellite['status'], list):
                data = 'Multiple values for "https://schema.space/metasat/status" or multiple \
                        entries with the same "@id" and different \
                        "https://schema.space/metasat/status" values'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            if isinstance(satellite['name'], list):
                data = 'Multiple values for "https://schema.space/metasat/name" or multiple \
                        entries with the same "@id" and different \
                        "https://schema.space/metasat/name" values'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            if isinstance(satellite['citation'], list):
                data = 'Multiple values for "https://schema.org/citation" or multiple \
                        entries with the same "@id" and different \
                        "https://schema.org/citation" values'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)

            satellite_data['name'] = satellite['name']
            satellite_data['status'] = satellite['status']
            satellite_data['citation'] = satellite['citation']
            satellite_data['created_by'] = request.user.pk

            if 'norad_cat_id' in satellite:
                if isinstance(satellite['norad_cat_id'], list):
                    data = 'Multiple values for "https://schema.space/metasat/noradID" or \
                            multiple entries with the same "@id" and different \
                            "https://schema.space/metasat/noradId" values'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                satellite_data['norad_cat_id'] = satellite['norad_cat_id']

            if 'names' in satellite:
                if isinstance(satellite['norad_cat_id'], list):
                    satellite_data['names'] = '\r\n'.join(satellite['names'])
                else:
                    satellite_data['names'] = satellite['names']

            if create_satellite_identifier:
                satellite_identifier = SatelliteIdentifier.objects.create()
                satellite_data['satellite_identifier'] = satellite_identifier.pk

            satellites_data.append(satellite_data)

        serializer = serializers.SatelliteEntrySerializer(
            data=satellites_data, many=True, allow_empty=True
        )
        if serializer.is_valid():
            serializer.save()
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        return Response(status=status.HTTP_201_CREATED)


@extend_schema_view(
    list=extend_schema(
        parameters=[
            OpenApiParameter(
                name='satellite__norad_cat_id',
                description='NORAD ID of a satellite to filter telemetry data for',
                examples=[ISS_EXAMPLE],
            ),
            OpenApiParameter(
                name='status',
                description='Filter by transmitter status: ' + ' '.join(TRANSMITTER_STATUS),
                required=False,
                type=OpenApiTypes.STR,
                examples=[OpenApiExample('active', value='\'active\'')]
            ),
            OpenApiParameter(
                name='service',
                description='Filter by transmitter service: ' + ' '.join(SERVICE_TYPE),
                required=False,
                type=OpenApiTypes.STR,
                examples=[OpenApiExample('Amateur', value='\'Amateur\'')]
            ),
            OpenApiParameter(
                name='type',
                description='Filter by transmitter type: ' + ' '.join(TRANSMITTER_TYPE),
                required=False,
                type=OpenApiTypes.STR,
                examples=[OpenApiExample('Transmitter', value='\'Transmitter\'')]
            ),
        ],
    ),
)
class TransmitterViewSet(  # pylint: disable=R0901
        CachedListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
        viewsets.GenericViewSet):
    """
    View into the Transmitter entities in the SatNOGS DB database.
    Transmitters are inclusive of Transceivers and Transponders
    """
    renderer_classes = [
        JSONRenderer, BrowsableAPIRendererWithoutForms, JSONLDRenderer, BrowserableJSONLDRenderer
    ]
    parser_classes = [JSONLDParser]
    queryset = Transmitter.objects.select_related(
        'satellite__satellite_identifier', 'satellite__satellite_entry'
    ).filter(
        satellite__satellite_entry__approved=True, satellite__associated_satellite__isnull=True
    ).exclude(status='invalid')
    serializer_class = serializers.TransmitterSerializer
    filterset_class = filters.TransmitterViewFilter
    lookup_field = 'uuid'
    cache_key = TransmitterEntry.CacheOptions.default_cache_key

    def create(self, request, *args, **kwargs):  # noqa: C901; pylint: disable=R0911,R0912,R0915
        """
        Creates a transmitter suggestion.
        """
        transmitters_data = []
        for transmitter_entry in request.data['@graph']:
            if 'transmitter' not in transmitter_entry:
                data = 'Transmitter Entry without "transmitter" key'
                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            if not transmitter_entry['transmitter']:
                data = 'One or more of the required fields are missing.\n Required fields: \
                        description, status, citation, service, satellite'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)
            transmitter = transmitter_entry['transmitter']

            transmitter_data = {}
            if "@id" not in transmitter:
                data = 'Missing "@id" for one or more entries'
                return Response(data, status=status.HTTP_400_BAD_REQUEST)

            if 'uuid' in transmitter:
                if isinstance(transmitter['uuid'], list):
                    data = 'Multiple values for "http://schema.org/identifier" or multiple \
                            entries with the same "@id" and different \
                            "http://schema.org/identifier" values'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                transmitter_uuid = transmitter['uuid']
                if Transmitter.objects.filter(uuid=transmitter_uuid).exists():
                    transmitter_data['uuid'] = transmitter_uuid

            transmitter_data['description'] = transmitter['description']
            transmitter_data['status'] = transmitter['status']
            transmitter_data['citation'] = transmitter['citation']
            transmitter_data['service'] = transmitter['service']
            transmitter_data['created_by'] = request.user.pk

            try:
                if transmitter['satellite']:
                    if isinstance(transmitter['satellite'], list):
                        data = 'Multiple values for "https://schema.space/metasat/satellite" \
                                or multiple entries with the same "@id" and different \
                                "https://schema.space/metasat/satellite" values'

                        return Response(data, status=status.HTTP_400_BAD_REQUEST)
                    transmitter_data['satellite'] = Satellite.objects.get(
                        satellite_entry__norad_cat_id=transmitter['satellite']['norad_cat_id']
                    ).pk
                else:
                    data = 'Missing NORAD ID value for Satellite'
                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
            except Satellite.DoesNotExist:
                data = 'Unknown NORAD ID: {}'.format(transmitter['satellite']['norad_cat_id'])
                return Response(data, status=status.HTTP_400_BAD_REQUEST)

            if 'baud' in transmitter:
                transmitter_data['baud'] = transmitter['baud']

            if 'invert' in transmitter:
                transmitter_data['invert'] = transmitter['invert']

            if 'uplink' not in transmitter and 'downlink' not in transmitter:
                data = 'Missing "https://schema.space/metasat/uplink" or \
                            "https://schema.space/metasat/downlink"'

                return Response(data, status=status.HTTP_400_BAD_REQUEST)

            if 'uplink' in transmitter:
                if isinstance(transmitter['uplink'], list):
                    data = 'Multiple values for "https://schema.space/metasat/uplink" or multiple \
                            entries with the same "@id" and different \
                            "https://schema.space/metasat/uplink" values'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if 'frequency' not in transmitter['uplink']:
                    data = 'Missing "https://schema.space/metasat/frequency" from \
                            "https://schema.space/metasat/uplink" value'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if isinstance(transmitter['uplink']['frequency'], int):
                    transmitter_data['type'] = 'Transceiver'
                    transmitter_data['uplink_low'] = transmitter['uplink']['frequency']
                else:
                    transmitter_data['type'] = 'Transponder'
                    if 'minimum' not in transmitter['uplink'][
                            'frequency'] or 'maximum' not in transmitter['uplink']['frequency']:
                        data = 'Missing "https://schema.org/minimum" or \
                                "https://schema.org/maximum" from \
                                "https://schema.space/metasat/frequency" value of \
                                "https://schema.space/metasat/uplink"'

                        return Response(data, status=status.HTTP_400_BAD_REQUEST)
                    transmitter_data['uplink_low'] = transmitter['uplink']['frequency']['minimum']
                    transmitter_data['uplink_high'] = transmitter['uplink']['frequency']['maximum']
                if 'mode' in transmitter['uplink']:
                    try:
                        transmitter_data['uplink_mode'] = Mode.objects.get(
                            name=transmitter['uplink']['mode']
                        ).pk
                    except Mode.DoesNotExist:
                        data = 'Unknown Mode: {}'.format(transmitter['uplink']['mode'])
                        return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if 'drift' in transmitter['uplink']:
                    transmitter_data['uplink_drift'] = transmitter['uplink']['drift']
            else:
                transmitter_data['type'] = 'Transmitter'

            if 'downlink' in transmitter:
                if isinstance(transmitter['downlink'], list):
                    data = 'Multiple values for "https://schema.space/metasat/downlink" or \
                            multiple entries with the same "@id" and different \
                            "https://schema.space/metasat/downlink" values'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if 'frequency' not in transmitter['downlink']:
                    data = 'Missing "https://schema.space/metasat/frequency" from \
                            "https://schema.space/metasat/downlink" value'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if isinstance(transmitter['downlink']['frequency'],
                              int) and not transmitter_data['type'] == 'Transponder':
                    transmitter_data['downlink_low'] = transmitter['downlink']['frequency']
                elif transmitter_data['type'] == 'Transponder':
                    if 'minimum' not in transmitter['downlink'][
                            'frequency'] or 'maximum' not in transmitter['downlink']['frequency']:
                        data = 'Missing "https://schema.org/minimum" or \
                                "https://schema.org/maximum" from \
                                "https://schema.space/metasat/frequency" value of \
                                "https://schema.space/metasat/downlink"'

                        return Response(data, status=status.HTTP_400_BAD_REQUEST)
                    transmitter_data['downlink_low'] = transmitter['downlink']['frequency'][
                        'minimum']
                    transmitter_data['downlink_high'] = transmitter['downlink']['frequency'][
                        'maximum']
                else:
                    data = 'Expected integer for "https://schema.space/metasat/frequency" value \
                            of "https://schema.space/metasat/downlink"'

                    return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if 'mode' in transmitter['downlink']:
                    try:
                        transmitter_data['downlink_mode'] = Mode.objects.get(
                            name=transmitter['downlink']['mode']
                        ).pk
                    except Mode.DoesNotExist:
                        data = 'Unknown Mode: {}'.format(transmitter['downlink']['mode'])
                        return Response(data, status=status.HTTP_400_BAD_REQUEST)
                if 'drift' in transmitter['downlink']:
                    transmitter_data['downlink_drift'] = transmitter['downlink']['drift']
            transmitters_data.append(transmitter_data)

        serializer = serializers.TransmitterEntrySerializer(
            data=transmitters_data, many=True, allow_empty=True
        )
        if serializer.is_valid():
            serializer.save()
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        return Response(status=status.HTTP_201_CREATED)


class OpticalObservationViewSet(  # pylint: disable=R0901
        mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
        viewsets.GenericViewSet):
    """ View for Optical Identifications"""
    queryset = OpticalObservation.objects.all().order_by("-start")
    permission_classes = [IsAuthenticatedOrOptions]
    parser_classes = [MultiPartParser]
    filterset_class = filters.OpticalObservationViewFilter

    def get_serializer_class(self):
        if self.action == 'create':
            return OpticalObservationCreateSerializer
        return OpticalObservationSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        try:
            instance = serializer.save()
            headers = self.get_success_headers(serializer.data)
            ret_serializer = OpticalObservationSerializer(instance, context={"request": request})
            return Response(ret_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
        except ValueError as err:
            return Response({"error": str(err)}, status=status.HTTP_400_BAD_REQUEST)
        except IntegrityError:
            return Response(
                {"error": "Observation with the same start time and station_id already exists"},
                status=status.HTTP_403_FORBIDDEN
            )


class LatestTleSetViewSet(  # pylint: disable=R0901
        CachedListModelMixin, viewsets.ReadOnlyModelViewSet):
    """
    Read-only view into the most recent two-line elements (TLE) in the SatNOGS DB
    database
    """
    renderer_classes = [JSONRenderer, BrowsableAPIRendererWithoutForms, TLERenderer]
    queryset = LatestTleSet.objects.all().select_related(
        'satellite__satellite_identifier'
    ).select_related('satellite__satellite_entry').exclude(
        latest_distributable__isnull=True
    ).annotate(
        tle0=F('latest_distributable__tle0'),
        tle1=F('latest_distributable__tle1'),
        tle2=F('latest_distributable__tle2'),
        tle_source=F('latest_distributable__tle_source'),
        updated=F('latest_distributable__updated')
    )
    serializer_class = serializers.LatestTleSetSerializer
    filterset_class = filters.LatestTleSetViewFilter

    def get_cache_key(self, request):
        """Returns the appropriate cache key based on user permissions"""
        if self.request.user.has_perm('base.access_all_tles'):
            return "latesttleset_with_perms"
        return "latesttleset_without_perms"

    def get_queryset(self):
        """
        Returns latest TLE queryset depending on user permissions
        """
        if self.request.user.has_perm('base.access_all_tles'):
            return LatestTleSet.objects.all().select_related(
                'satellite__satellite_identifier'
            ).select_related('satellite__satellite_entry').exclude(latest__isnull=True).annotate(
                tle0=F('latest__tle0'),
                tle1=F('latest__tle1'),
                tle2=F('latest__tle2'),
                tle_source=F('latest__tle_source'),
                updated=F('latest__updated')
            )
        return self.queryset


@extend_schema_view(
    list=extend_schema(
        parameters=[
            OpenApiParameter(
                name='app_source',
                description='The submission source for the telemetry frames: manual (a manual \
                             upload/entry), network (SatNOGS Network observations), or sids \
                             (legacy API submission)',
            ),
            OpenApiParameter(
                name='observer',
                description='(string) name of the observer (submitter) to retrieve telemetry data \
                            from'
            ),
            OpenApiParameter(
                name='satellite',
                description='NORAD ID of a satellite to filter telemetry data for',
                examples=[ISS_EXAMPLE],
            ),
            OpenApiParameter(name='transmitter', description='Not currently in use'),
        ],
    ),
)
class TelemetryViewSet(  # pylint: disable=R0901,R0912,R0915
        mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
        viewsets.GenericViewSet):
    """
    View into the Telemetry objects in the SatNOGS DB database. Currently,
    this table is inclusive of all data collected from satellite downlink
    observations
    """
    renderer_classes = [
        JSONRenderer, BrowsableAPIRendererWithoutForms, JSONLDRenderer, BrowserableJSONLDRenderer
    ]
    queryset = (
        DemodData.objects.all().prefetch_related(
            "satellite", "transmitter", "satellite__satellite_identifier",
            "satellite__satellite_entry", "satellite__associated_with"
        )
    )
    serializer_class = serializers.TelemetrySerializer
    filterset_class = filters.TelemetryViewFilter
    permission_classes = [SafeMethodsWithPermission]
    throttle_classes = [
        GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle,
        GetTelemetryViolatorThrottle
    ]
    parser_classes = (FormParser, MultiPartParser, FileUploadParser)
    pagination_class = pagination.DemodDataCursorPagination

    def list(self, request, *args, **kwargs):
        """
        Lists data from satellite if they are filtered by NORAD ID or Satellite ID. Also logs the
        requests if it is set to do so.
        """
        satellite = request.query_params.get('satellite', None)
        sat_id = request.query_params.get('sat_id', None)

        if not (satellite or sat_id):
            data = {
                'detail': (
                    'For getting data please use either satellite(NORAD ID) filter or'
                    'sat_id(Satellite ID) filter'
                ),
                'results': None
            }
            response = Response(data, status=status.HTTP_400_BAD_REQUEST)
            response.exception = True
            return response

        if settings.LOG_TELEMETRY_REQUESTS:
            user_id = str(request.user.id)
            remote_address = str(request.META.get("REMOTE_ADDR"))
            x_forwarded_for = str(request.META.get("HTTP_X_FORWARDED_FOR"))
            timestamp = now().isoformat()
            request_data = user_id + ';' + remote_address + ';' + x_forwarded_for + ';' + timestamp
            cache.set(
                'telemetry_log_' + user_id + '_' + timestamp, request_data,
                settings.TELEMETRY_LOGS_TIME_TO_LIVE
            )
        return super().list(request, *args, **kwargs)

    @extend_schema(
        responses={'201': None},  # None
    )
    def create(self, request, *args, **kwargs):
        """
        Creates a frame of telemetry data from a satellite observation.
        """
        # pylint: disable=R0914
        data = {}

        norad_id = request.data.get('noradID')

        try:
            if norad_id:
                satellite = Satellite.objects.get(satellite_entry__norad_cat_id=norad_id)
            else:
                raise ValueError
        except Satellite.DoesNotExist:
            satellite_identifier = SatelliteIdentifier.objects.create()
            satellite_entry = SatelliteEntry.objects.create(
                norad_cat_id=norad_id,
                name='New Satellite',
                satellite_identifier=satellite_identifier,
                created=now()
            )
            satellite = Satellite.objects.create(
                satellite_identifier=satellite_identifier, satellite_entry=satellite_entry
            )
            update_satellite_name.delay(int(norad_id))
        except ValueError:
            return Response(status=status.HTTP_400_BAD_REQUEST)

        data['satellite'] = satellite.id
        data['station'] = request.data.get('source')
        data['timestamp'] = request.data.get('timestamp')
        if request.data.get('version'):
            data['version'] = request.data.get('version')
        observation_id = ''
        if request.data.get('observation_id'):
            observation_id = request.data.get('observation_id')
            data['observation_id'] = observation_id
        station_id = ''
        if request.data.get('station_id'):
            station_id = request.data.get('station_id')
            data['station_id'] = station_id

        try:
            lat = request.data['latitude']
            lng = request.data['longitude']
        except KeyError as err:
            return Response(
                f"Missing request parameter: '{err.args[0]}'", status=status.HTTP_400_BAD_REQUEST
            )

        # Convert coordinates to omit N-S and W-E designators
        try:
            if any(x.isalpha() for x in lat):
                data['lat'] = (-float(lat[:-1]) if ('S' in lat) else float(lat[:-1]))
            else:
                data['lat'] = float(lat)
            if any(x.isalpha() for x in lng):
                data['lng'] = (-float(lng[:-1]) if ('W' in lng) else float(lng[:-1]))
            else:
                data['lng'] = float(lng)
        except ValueError:
            return Response(status=status.HTTP_400_BAD_REQUEST)

        # Network or SiDS submission?
        if request.data.get('satnogs_network'):
            data['app_source'] = 'network'
        else:
            data['app_source'] = 'sids'

        # Create file out of frame string
        frame = ContentFile(request.data.get('frame'), name='sids')
        data['payload_frame'] = frame
        # Create observer
        qth = gridsquare(data['lat'], data['lng'])
        observer = '{0}-{1}'.format(data['station'], qth)
        data['observer'] = observer

        serializer = serializers.SidsSerializer(data=data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        # Run task to decode the current frame
        decode_current_frame.delay(satellite.satellite_identifier.sat_id, serializer.instance.pk)

        # Run task to publish the current frame via ZeroMQ
        if settings.ZEROMQ_ENABLE:
            publish_current_frame.delay(
                request.data.get('timestamp'), request.data.get('frame'), observer, {
                    'norad_id': norad_id,
                    'observation_id': observation_id,
                    'station_id': station_id
                }
            )

        return Response(status=status.HTTP_201_CREATED)


@extend_schema_view(
    list=extend_schema(
        parameters=[
            OpenApiParameter(
                'network_obs_id',
                OpenApiTypes.INT64,
                required=False,
                description='Given a SatNOGS Network observation ID, this will return any \
                             artifacts files associated with the observation.'
            ),
        ],
    ),
    retrieve=extend_schema(
        parameters=[
            OpenApiParameter(
                'id',
                OpenApiTypes.URI,
                OpenApiParameter.PATH,
                description='The ID for the requested artifact entry in DB'
            ),
        ],
    ),
)
class ArtifactViewSet(  # pylint: disable=R0901
        mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
        viewsets.GenericViewSet):
    """
    Artifacts are file-formatted objects collected from a satellite observation.
    """
    queryset = Artifact.objects.all()
    filterset_class = filters.ArtifactViewFilter
    permission_classes = [IsAuthenticatedOrOptions]
    parser_classes = (FormParser, MultiPartParser)
    pagination_class = pagination.LinkedHeaderPageNumberPagination

    def get_serializer_class(self):
        """Returns the right serializer depending on http method that is used"""
        if self.action == 'create':
            return serializers.NewArtifactSerializer
        return serializers.ArtifactSerializer

    def create(self, request, *args, **kwargs):
        """
        Creates observation artifact from an [HDF5 formatted file][hdf5ref]
        * Requires session or key authentication to create an artifact

        [hdf5ref]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format
        """
        serializer = self.get_serializer(data=request.data)
        try:
            if serializer.is_valid():
                data = serializer.save()
                http_response = {}
                http_response['id'] = data.id
                response = Response(http_response, status=status.HTTP_200_OK)
            else:
                data = serializer.errors
                response = Response(data, status=status.HTTP_400_BAD_REQUEST)
        except (ValidationError, ValueError, OSError) as error:
            response = Response(str(error), status=status.HTTP_400_BAD_REQUEST)
        return response
