import abc
import json

from rest_framework.response import Response
from rest_framework.views import APIView

from django.contrib.gis.gdal import CoordTransform, SpatialReference
from django.db.models import Q, QuerySet

from core.models.assemblies import Assembly
from core.models.conference import Conference, ConferenceExportCache
from core.models.map import MapPOI

from api.permissions import IsConferenceService, IsSuperUser
from api.views.mixins import ConferenceSlugMixin

_cts = {}  # cache of CoordTransforms (if needed)


q_publishable_location_states = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL])


def get_exportable_assemblies(conference: Conference) -> QuerySet[Assembly]:
    """Fetches all assemblies in the given conference with publish-able location."""
    exportable_states = [*Assembly.PUBLIC_STATES, Assembly.State.HIDDEN]
    return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_publishable_location_states)


def get_field_geojson(value, srid: int, ct_cache: dict | None = None):
    if value.srid != srid:
        if ct_cache is None:
            ct_cache = {}
        if value.srid not in ct_cache:
            srs = SpatialReference(srid)
            ct_cache[value.srid] = CoordTransform(value.srs, srs)
        value.transform(ct_cache[value.srid])
    return json.loads(value.geojson)


class PoiExportView(ConferenceSlugMixin, APIView):
    def get_geojson(self):
        srid = 4326  # RFC7946 mandates WGS84
        ct_cache = {}
        features = []
        result = {
            'type': 'FeatureCollection',
            'features': features,
        }

        for poi in MapPOI.objects.filter(conference=self.conference, visible=True).exclude(location_point=None):
            feature = {
                'type': 'Feature',
                'id': str(poi.id),
                'geometry': get_field_geojson(poi.location_point, srid=srid, ct_cache=ct_cache),
                'properties': {
                    'floor': poi.get_location_floor_index(),
                    'type': 'poi',
                    'name': poi.name,
                    'official': poi.is_official,
                },
            }
            features.append(feature)

        return result

    def get(self, request, *args, **kwargs):
        cache_id = 'geojson-poi'

        return ConferenceExportCache.handle_http_request(
            request=request,
            conference=self.conference,
            entry_type=ConferenceExportCache.Type.MAP,
            ident=cache_id,
            content_type='application/geo+json',
            result_func=lambda: json.dumps(self.get_geojson()),
        )


class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta):
    geometry_field = None

    def get_queryset(self):
        return get_exportable_assemblies(conference=self.conference)

    def get_geometry_field(self, obj):
        return getattr(obj, self.geometry_field)

    def get_geojson(self):
        srid = 4326  # RFC7946 mandates WGS84
        ct_cache = {}
        features = []
        result = {
            'type': 'FeatureCollection',
            'features': features,
        }

        for assembly in self.get_queryset().select_related('parent').all():
            geometry = self.get_geometry_field(assembly)
            if geometry is None:
                continue

            feature = {
                'type': 'Feature',
                'id': assembly.slug,
                'geometry': get_field_geojson(geometry, srid=srid, ct_cache=ct_cache),
                'properties': {
                    'floor': assembly.get_location_floor_index(),
                    'type': 'assembly',
                    'name': assembly.name,
                    'official': assembly.is_official,
                    'cluster': assembly.is_cluster,
                    'parent': assembly.parent.slug if assembly.parent else None,
                },
            }
            features.append(feature)

        return result

    def get(self, request, *args, **kwargs):
        cache_id = 'geojson-' + self.geometry_field

        return ConferenceExportCache.handle_http_request(
            request=request,
            conference=self.conference,
            entry_type=ConferenceExportCache.Type.MAP,
            ident=cache_id,
            content_type='application/geo+json',
            result_func=lambda: json.dumps(self.get_geojson()),
        )


class AssembliesPoiExportView(AssembliesExportView):
    geometry_field = 'location_point'


class AssembliesAreasExportView(AssembliesExportView):
    geometry_field = 'location_boundaries'


class C3NavExportView(ConferenceSlugMixin, APIView):
    permission_classes = [IsConferenceService | IsSuperUser]
    required_service_classes = ['c3nav']

    def get(self, request, *args, **kwargs):
        from core.templatetags.hub_absolute import hub_absolute  # pylint: disable=import-outside-toplevel

        data = []

        qs = get_exportable_assemblies(self.conference)
        if request.GET.get('all') != '1':
            qs = qs.exclude(location_data__point=None, location_data__boundaries=None)
        for assembly in qs.all():  # type: Assembly
            loc_data = assembly.location_data or {}
            data.append(
                {
                    'type': 'assembly',
                    'id': str(assembly.pk),
                    'slug': assembly.slug,
                    'name': assembly.name,
                    'is_official': assembly.is_official,
                    'description': {'de': assembly.description_de, 'en': assembly.description_en},
                    'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug, i18n=False),
                    'parent_id': assembly.parent_id,
                    'children': get_exportable_assemblies(conference=self.conference).filter(parent=assembly).values_list('slug', flat=True)
                    if assembly.is_cluster
                    else None,
                    'rooms': assembly.rooms.filter(q_publishable_location_states).exclude(blocked=True).values_list('slug', flat=True),
                    'floor': assembly.get_location_floor_index(),
                    'is_preview': assembly.location_state != Assembly.LocationState.FINAL,
                    'location': loc_data.get('point'),  # assembly.get_location_point_xy(),
                    'polygons': loc_data.get('boundaries'),  # assembly.get_location_boundaries_xy(),
                    'tags': assembly.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
                }
            )

            for project in assembly.projects.conference_accessible(self.conference):
                data.append(
                    {
                        'type': 'project',
                        'id': str(project.pk),
                        'slug': project.slug,
                        'name': project.name,
                        'description': {'de': project.description_de, 'en': project.description_en},
                        'public_url': hub_absolute('plainui:project', slug=project.slug, i18n=False),
                        'assembly_id': str(assembly.pk),
                        'floor': assembly.get_location_floor_index(),  # TODO: allow project-specific location floor
                        'is_preview': assembly.location_state != Assembly.LocationState.FINAL,
                        'location': loc_data.get('point'),  # TODO: allow project-specific location point
                        'location_text': project.location,
                        'tags': project.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
                    }
                )

        for room in self.conference.rooms.filter(q_publishable_location_states).exclude(blocked=True):
            data.append(
                {
                    'type': 'room',
                    'id': str(room.pk),
                    'name': room.name,
                    'slug': room.slug,
                    'is_official': room.is_official,
                    'in_public_fahrplan': room.is_public_fahrplan,
                    'description': {'de': room.description_de, 'en': room.description_en},
                    'public_url': hub_absolute('plainui:room', slug=room.slug, i18n=False),
                    'is_preview': room.location_state != Assembly.LocationState.FINAL,
                    'floor': room.get_location_floor_index(),
                    'location': room.get_location_point_xy(),
                    'polygons': room.get_location_boundaries_xy(),
                    'tags': room.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
                }
            )

        for poi in self.conference.pois.filter(visible=True):  # type: MapPOI
            data.append(
                {
                    'type': 'poi',
                    'id': str(poi.pk),
                    'name': {'de': poi.name_de, 'en': poi.name_en},
                    'is_official': poi.is_official,
                    'description': {'de': poi.description_de, 'en': poi.description_en},
                    'is_preview': False,
                    'floor': poi.get_location_floor_index(),
                    'location': poi.get_location_point_xy(),
                }
            )

        return Response(data)