import hashlib
import hmac
import json
import logging

from core.api.serializers_.jobs import JobSerializer
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponseBadRequest
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import OpenApiResponse
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets import NetBoxReadOnlyModelViewSet
from netbox.plugins.utils import get_plugin_config
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from users.models import User
from utilities.query import count_related

from .serializers import EmptySerializer
from .serializers import IPFabricEndpointSerializer
from .serializers import IPFabricFilterExpressionSerializer
from .serializers import IPFabricFilterSerializer
from .serializers import IPFabricIngestionIssueSerializer
from .serializers import IPFabricIngestionSerializer
from .serializers import IPFabricRelationshipFieldSerializer
from .serializers import IPFabricSnapshotSerializer
from .serializers import IPFabricSourceSerializer
from .serializers import IPFabricSyncSerializer
from .serializers import IPFabricTransformFieldSerializer
from .serializers import IPFabricTransformMapGroupSerializer
from .serializers import IPFabricTransformMapSerializer
from .serializers import IPFabricWebhookPayloadSerializer
from ipfabric_netbox.filtersets import IPFabricRelationshipFieldFilterSet
from ipfabric_netbox.filtersets import IPFabricSnapshotFilterSet
from ipfabric_netbox.filtersets import IPFabricSourceFilterSet
from ipfabric_netbox.filtersets import IPFabricTransformFieldFilterSet
from ipfabric_netbox.filtersets import IPFabricTransformMapFilterSet
from ipfabric_netbox.models import IPFabricData
from ipfabric_netbox.models import IPFabricEndpoint
from ipfabric_netbox.models import IPFabricFilter
from ipfabric_netbox.models import IPFabricFilterExpression
from ipfabric_netbox.models import IPFabricIngestion
from ipfabric_netbox.models import IPFabricIngestionIssue
from ipfabric_netbox.models import IPFabricRelationshipField
from ipfabric_netbox.models import IPFabricSnapshot
from ipfabric_netbox.models import IPFabricSource
from ipfabric_netbox.models import IPFabricSync
from ipfabric_netbox.models import IPFabricTransformField
from ipfabric_netbox.models import IPFabricTransformMap
from ipfabric_netbox.models import IPFabricTransformMapGroup

logger = logging.getLogger("ipfabric_netbox.api.views")


class IPFabricEndpointViewSet(NetBoxReadOnlyModelViewSet):
    queryset = IPFabricEndpoint.objects.all()
    serializer_class = IPFabricEndpointSerializer


class IPFabricTransformMapGroupViewSet(NetBoxModelViewSet):
    queryset = IPFabricTransformMapGroup.objects.all()
    serializer_class = IPFabricTransformMapGroupSerializer


class IPFabricTransformMapViewSet(NetBoxModelViewSet):
    queryset = IPFabricTransformMap.objects.all()
    serializer_class = IPFabricTransformMapSerializer
    filterset_class = IPFabricTransformMapFilterSet


class IPFabricTransformFieldViewSet(NetBoxModelViewSet):
    queryset = IPFabricTransformField.objects.all()
    serializer_class = IPFabricTransformFieldSerializer
    filterset_class = IPFabricTransformFieldFilterSet


class IPFabricRelationshipFieldViewSet(NetBoxModelViewSet):
    queryset = IPFabricRelationshipField.objects.all()
    serializer_class = IPFabricRelationshipFieldSerializer
    filterset_class = IPFabricRelationshipFieldFilterSet


class IPFabricSyncViewSet(NetBoxModelViewSet):
    queryset = IPFabricSync.objects.all()
    serializer_class = IPFabricSyncSerializer

    @extend_schema(
        methods=["post"],
        request=EmptySerializer(),
        responses={201: JobSerializer()},
    )
    @action(detail=True, methods=["post"])
    def sync(self, request, pk):
        if not request.user.has_perm("ipfabric_netbox.sync_ipfabricsync"):
            raise PermissionDenied(
                "This user does not have permission to sync IPFabricSync."
            )
        sync = self.get_object()
        if not sync.ready_for_sync:
            return HttpResponseBadRequest(
                f"Sync '{sync.name}' is not ready to be synced."
            )
        job = sync.enqueue_sync_job(user=request.user, adhoc=True)
        return Response(
            JobSerializer(job, context={"request": request}).data, status=201
        )

    @extend_schema(
        methods=["post"],
        request=IPFabricWebhookPayloadSerializer,
        responses={
            status.HTTP_202_ACCEPTED: OpenApiResponse(description="Sync job queued."),
            status.HTTP_200_OK: OpenApiResponse(
                description="Event ignored (test flag or unsupported action/status)."
            ),
            status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                description="Bad request (invalid JSON, unknown configured username, or missing snapshot ID)."
            ),
            status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                description="Unauthorized (no secret configured or invalid X-IPF-Signature)."
            ),
            status.HTTP_404_NOT_FOUND: OpenApiResponse(description="Sync not found."),
            status.HTTP_409_CONFLICT: OpenApiResponse(
                description="Sync is currently running; try again later."
            ),
        },
        description=(
            "Receive an IP Fabric snapshot webhook and trigger an ingestion for this specific Sync. "
            "The request must carry a valid HMAC-SHA256 signature in the X-IPF-Signature header "
            "(computed by IP Fabric using the secret configured in PLUGINS_CONFIG). "
            "Only 'discover' and 'load' actions with status 'completed' and test=false are processed."
        ),
    )
    @action(
        detail=True,
        methods=["post"],
        url_path="webhook",
        permission_classes=[AllowAny],
        authentication_classes=[],
    )
    def webhook(self, request, pk):
        # ------------------------------------------------------------------
        # 1. Look up the per-sync webhook config entry
        # ------------------------------------------------------------------
        webhook_secrets = get_plugin_config("ipfabric_netbox", "webhook_secrets", {})
        entry = webhook_secrets.get(int(pk))
        if not entry or not (secret := entry.get("secret")):
            logger.warning("Webhook [sync=%s]: no secret configured.", pk)
            return Response(
                {"detail": "No webhook secret configured for this sync."},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # ------------------------------------------------------------------
        # 2. Verify HMAC-SHA256 signature (X-IPF-Signature header)
        # ------------------------------------------------------------------
        signature = request.META.get("HTTP_X_IPF_SIGNATURE", "")
        if not signature:
            logger.warning("Webhook [sync=%s]: missing X-IPF-Signature header.", pk)
            return Response(
                {"detail": "Missing X-IPF-Signature header."},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        raw_body = request.body
        computed = hmac.new(
            secret.encode("utf-8"), raw_body, hashlib.sha256
        ).hexdigest()
        if not hmac.compare_digest(computed, signature):
            logger.warning("Webhook [sync=%s]: invalid HMAC signature.", pk)
            return Response(
                {"detail": "Invalid HMAC signature."},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # ------------------------------------------------------------------
        # 3. Resolve the NetBox user that will own the job
        # ------------------------------------------------------------------
        if not (username := entry.get("username", "")):
            logger.warning("Webhook [sync=%s]: no username configured.", pk)
            return Response(
                {"detail": "No username configured for this sync's webhook secret."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            logger.warning("Webhook [sync=%s]: username '%s' not found.", pk, username)
            return Response(
                {"detail": f"Configured username {username} not found."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # ------------------------------------------------------------------
        # 4. Parse payload and apply event filters
        # ------------------------------------------------------------------
        try:
            payload = json.loads(raw_body)
        except (json.JSONDecodeError, ValueError):
            logger.warning("Webhook [sync=%s]: invalid JSON payload.", pk)
            return Response(
                {"detail": "Invalid JSON payload."}, status=status.HTTP_400_BAD_REQUEST
            )

        # Ignore test events fired from the IP Fabric UI
        if payload.get("test") is True:
            logger.debug("Webhook [sync=%s]: test event ignored.", pk)
            return Response(
                {"detail": "Test event ignored."}, status=status.HTTP_200_OK
            )

        # Only react to discover / load actions
        if (event_action := payload.get("action")) not in ("discover", "load"):
            logger.debug("Webhook [sync=%s]: action '%s' ignored.", pk, event_action)
            return Response(
                {"detail": f"Action '{event_action}' ignored."},
                status=status.HTTP_200_OK,
            )

        # Only react to completed status
        event_status = payload.get("status")
        if event_status != "completed":
            logger.debug("Webhook [sync=%s]: status '%s' ignored.", pk, event_status)
            return Response(
                {"detail": f"Status '{event_status}' ignored."},
                status=status.HTTP_200_OK,
            )

        # Snapshot object must be present with an ID
        snapshot_id = (payload.get("snapshot") or {}).get("id")
        if not snapshot_id:
            logger.warning("Webhook [sync=%s]: no snapshot ID in payload.", pk)
            return Response(
                {"detail": "No snapshot ID in payload."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # ------------------------------------------------------------------
        # 5. Fetch sync and guard against concurrent runs
        # ------------------------------------------------------------------
        try:
            sync = IPFabricSync.objects.get(pk=pk)
        except IPFabricSync.DoesNotExist:
            logger.warning("Webhook [sync=%s]: sync not found.", pk)
            return Response(
                {"detail": "Sync not found."}, status=status.HTTP_404_NOT_FOUND
            )

        if not sync.ready_for_sync:
            logger.warning(
                "Webhook [sync=%s]: '%s' is currently running.", pk, sync.name
            )
            return Response(
                {"detail": f"Sync {sync.name} is currently running, try again later."},
                status=status.HTTP_409_CONFLICT,
            )

        # ------------------------------------------------------------------
        # 6. Queue source sync (to pull the new snapshot) + ingestion sync
        # ------------------------------------------------------------------
        job = sync.enqueue_sync_job(
            adhoc=True, user=user, pending_snapshot_id=snapshot_id
        )

        logger.info(
            "Webhook [sync=%s]: queued job %s for snapshot '%s'.",
            pk,
            job.pk,
            snapshot_id,
        )
        return Response(
            {
                "detail": f"Sync job queued for snapshot '{snapshot_id}'.",
                "job_id": job.pk,
            },
            status=status.HTTP_202_ACCEPTED,
        )


class IPFabricFilterViewSet(NetBoxModelViewSet):
    queryset = IPFabricFilter.objects.all()
    serializer_class = IPFabricFilterSerializer


class IPFabricFilterExpressionViewSet(NetBoxModelViewSet):
    queryset = IPFabricFilterExpression.objects.all()
    serializer_class = IPFabricFilterExpressionSerializer


class IPFabricIngestionViewSet(NetBoxReadOnlyModelViewSet):
    queryset = IPFabricIngestion.objects.all()
    serializer_class = IPFabricIngestionSerializer


class IPFabricIngestionIssueViewSet(NetBoxReadOnlyModelViewSet):
    queryset = IPFabricIngestionIssue.objects.all()
    serializer_class = IPFabricIngestionIssueSerializer


class IPFabricSnapshotViewSet(NetBoxReadOnlyModelViewSet):
    queryset = IPFabricSnapshot.objects.all()
    serializer_class = IPFabricSnapshotSerializer
    filterset_class = IPFabricSnapshotFilterSet

    @action(detail=True, methods=["patch", "delete"], url_path="raw")
    def raw(self, request, pk):
        snapshot = self.get_object()
        if request.method == "DELETE":
            raw_data = IPFabricData.objects.filter(snapshot_data=snapshot)
            raw_data._raw_delete(raw_data.db)
            return Response({"status": "success"})
        elif request.method == "PATCH":
            with transaction.atomic():
                IPFabricData.objects.bulk_create(
                    [
                        IPFabricData(snapshot_data=snapshot, data=item["data"])
                        for item in request.data["data"]
                    ],
                    batch_size=5000,
                )
            return Response({"status": "success"})

    @action(detail=True, methods=["get"], url_path="sites")
    def sites(self, request, pk):
        q = request.GET.get("q", None)
        snapshot = IPFabricSnapshot.objects.get(pk=pk)
        new_sites = {"count": 0, "results": []}
        if snapshot.data:
            sites = snapshot.data.get("sites", None)
            num = 0
            if sites:
                for site in sites:
                    if q:
                        if q.lower() in site.lower():
                            new_sites["results"].append(
                                {"display": site, "name": site, "id": site}
                            )
                    else:
                        new_sites["results"].append(
                            {"display": site, "name": site, "id": site}
                        )
                    num += 1
                new_sites["count"] = num
                return Response(new_sites)
        else:
            return Response([])


class IPFabricSourceViewSet(NetBoxModelViewSet):
    queryset = IPFabricSource.objects.annotate(
        snapshot_count=count_related(IPFabricSnapshot, "source")
    )
    serializer_class = IPFabricSourceSerializer
    filterset_class = IPFabricSourceFilterSet

    @extend_schema(
        methods=["post"],
        request=EmptySerializer(),
        responses={201: JobSerializer()},
    )
    @action(detail=True, methods=["post"])
    def sync(self, request, pk):
        if not request.user.has_perm("ipfabric_netbox.sync_ipfabricsource"):
            raise PermissionDenied(
                "This user does not have permission to sync IPFabricSource."
            )
        source = self.get_object()
        if not source.ready_for_sync:
            return HttpResponseBadRequest(
                f"Source '{source.name}' is not ready to be synced."
            )
        job = source.enqueue_sync_job(request=request)
        return Response(
            JobSerializer(job, context={"request": request}).data, status=201
        )
