from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from django.db import IntegrityError from apps.accounts.permissions import CanRegisterActiveSite, IsExecutive, can_sync from apps.accounts.scoping import filter_active_sites_queryset from apps.farms.services.active_site_registry import ensure_hq_site_for_active_site from apps.sync.models import ActiveSite from apps.sync.serializers import ( ActiveSiteClaimSerializer, ActiveSiteSerializer, ActiveSiteWriteSerializer, SiteRegisterSerializer, ) class ActiveSiteViewSet(viewsets.ModelViewSet): """ List/read: scoped (director all; BUH managed GMs; GM own). Create (claim): GM and GM Admin claim a site-registered ActiveSite by code. Sync: GM and GM Admin only (stubs until mirror pull is implemented). Site dashboards create/fill ActiveSite via register-from-site (API key auth). Claiming attaches managed_by and ensures an HQ Site for the view list. Destroy (Hapus) unclaims only — does not delete the ActiveSite row. """ executive_sync_action = True http_method_names = ["get", "post", "put", "patch", "delete", "head", "options"] def get_queryset(self): qs = ActiveSite.objects.select_related("managed_by").all() return filter_active_sites_queryset(qs, self.request.user) def get_permissions(self): if self.action == "register_from_site": return [permissions.IsAuthenticated()] if self.action in {"create", "update", "partial_update", "destroy"}: return [CanRegisterActiveSite()] return [IsExecutive()] def get_serializer_class(self): if self.action == "register_from_site": return SiteRegisterSerializer if self.action == "create": return ActiveSiteClaimSerializer if self.action in {"update", "partial_update"}: return ActiveSiteWriteSerializer return ActiveSiteSerializer def create(self, request, *args, **kwargs): """GM claim-by-code: site must have registered first.""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) code = serializer.validated_data["code"] active_site = ActiveSite.objects.filter(code=code).first() if active_site is None: return Response( { "detail": ( "Site belum mendaftar dari dasbor site. " "Minta staf site mendaftar dulu dengan kode ini." ) }, status=status.HTTP_404_NOT_FOUND, ) owner_id = active_site.managed_by_id if owner_id and owner_id != request.user.pk: return Response( {"detail": "Kode sudah diklaim oleh GM lain."}, status=status.HTTP_409_CONFLICT, ) if owner_id != request.user.pk: active_site.managed_by = request.user active_site.save(update_fields=["managed_by", "updated_at"]) ensure_hq_site_for_active_site(active_site) return Response( ActiveSiteSerializer(active_site).data, status=status.HTTP_200_OK, ) def perform_update(self, serializer): active_site = serializer.save() if active_site.is_active and active_site.managed_by_id: ensure_hq_site_for_active_site(active_site) def destroy(self, request, *args, **kwargs): """ Hapus from daftar tampilan: unclaim only. Keeps the ActiveSite registry row (credentials / source_site_id) so the site dashboard registration is not wiped. Clears managed_by and unlinks any HQ Site.active_site pointer. """ from apps.farms.models import Site active_site = self.get_object() Site.objects.filter(active_site=active_site).update(active_site=None) active_site.managed_by = None active_site.save(update_fields=["managed_by", "updated_at"]) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=False, methods=["post"], url_path="register-from-site") def register_from_site(self, request): """ Machine endpoint: site dashboard creates/updates ActiveSite credentials. Does not set managed_by (GM claim is separate). One farm site_id maps to at most one ActiveSite. Re-register with a new code updates that row; it never inserts a second ActiveSite. """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data code = data["code"] source_site_id = data["site_id"] active_site = ActiveSite.objects.filter(source_site_id=source_site_id).first() if active_site is None: by_code = ActiveSite.objects.filter(code=code).first() if by_code is not None: if by_code.source_site_id and by_code.source_site_id != source_site_id: return Response( {"detail": "Kode sudah dipakai site lain."}, status=status.HTTP_409_CONFLICT, ) # Legacy row missing source_site_id: attach this site to it. active_site = by_code if active_site is None: try: active_site = ActiveSite.objects.create( code=code, name=data["name"], source_site_id=source_site_id, api_base_url=data["api_base_url"], api_key=data["api_key"], managed_by=None, is_active=True, ) except IntegrityError: # Concurrent register for same source_site_id or code. active_site = ActiveSite.objects.filter( source_site_id=source_site_id ).first() or ActiveSite.objects.filter(code=code).first() if active_site is None: return Response( {"detail": "Gagal mendaftarkan site — coba lagi."}, status=status.HTTP_409_CONFLICT, ) else: return Response( ActiveSiteSerializer(active_site).data, status=status.HTTP_201_CREATED, ) if ( code != active_site.code and ActiveSite.objects.filter(code=code).exclude(pk=active_site.pk).exists() ): return Response( {"detail": "Kode sudah dipakai site lain."}, status=status.HTTP_409_CONFLICT, ) active_site.code = code active_site.name = data["name"] active_site.source_site_id = source_site_id active_site.api_base_url = data["api_base_url"] active_site.api_key = data["api_key"] active_site.is_active = True try: active_site.save( update_fields=[ "code", "name", "source_site_id", "api_base_url", "api_key", "is_active", "updated_at", ] ) except IntegrityError: return Response( {"detail": "Site atau kode bentrok dengan pendaftaran lain."}, status=status.HTTP_409_CONFLICT, ) return Response(ActiveSiteSerializer(active_site).data, status=status.HTTP_200_OK) @action(detail=True, methods=["post"], url_path="sync") def sync(self, request, pk=None): if not can_sync(request.user): return Response({"detail": "Sync not allowed."}, status=status.HTTP_403_FORBIDDEN) active_site = self.get_object() if not active_site.is_active: return Response( {"detail": "Active site is inactive."}, status=status.HTTP_400_BAD_REQUEST, ) return Response( { "detail": "Active site sync not implemented yet.", "active_site": ActiveSiteSerializer(active_site).data, }, status=status.HTTP_501_NOT_IMPLEMENTED, ) @action(detail=False, methods=["post"], url_path="sync-all") def sync_all(self, request): if not can_sync(request.user): return Response({"detail": "Sync not allowed."}, status=status.HTTP_403_FORBIDDEN) sites = list(self.get_queryset().filter(is_active=True)) return Response( { "detail": "Active site sync-all not implemented yet.", "active_site_count": len(sites), "active_sites": ActiveSiteSerializer(sites, many=True).data, }, status=status.HTTP_501_NOT_IMPLEMENTED, )