from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query, status from app.core.dependencies import require_superuser from app.modules.sync import service from app.modules.sync.schemas import ( AckChangesRequest, AckChangesResponse, HeartbeatRequest, HeartbeatResponse, PairConfirmRequest, PairConfirmResponse, PairStartRequest, PullChangesRequest, PullChangesResponse, PushChangesRequest, PushChangesResponse, ResolveConflictRequest, SyncCapabilitiesResponse, ) from app.modules.sync.service import SyncServiceError from app.modules.sync.tenant import ( HubPrincipal, TenantContext, get_hub_auth, get_tenant_context, require_enterprise_zootech, ) from app.modules.users.models import User from app.core.dependencies import get_current_user router = APIRouter() def _map_error(exc: SyncServiceError) -> HTTPException: code = str(exc) status_code = status.HTTP_400_BAD_REQUEST if code in {"ENTERPRISE_ADMIN_ONLY", "ENTERPRISE_FORBIDDEN", "ENTERPRISE_ZOOTECH_ONLY"}: status_code = status.HTTP_403_FORBIDDEN if code in {"INVALID_PAIRING_CODE", "PAIRING_CODE_EXPIRED"}: status_code = status.HTTP_400_BAD_REQUEST if code == "CONFLICT_NOT_FOUND": status_code = status.HTTP_404_NOT_FOUND return HTTPException(status_code=status_code, detail=code) @router.post("/reports/push") def reports_push(body: PushChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PushChangesResponse: """Report domain ingest — same durability path as global push.""" return service.push_changes(hub, body) @router.post("/reports/refresh") def reports_refresh( farm_hub_id: str = Query(...), enterprise_id: str = Query(...), tenant: TenantContext = Depends(require_enterprise_zootech), ): if tenant.farm_hub_ids is not None and farm_hub_id not in tenant.farm_hub_ids: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="FARM_ACCESS_FORBIDDEN") return {"status": "queued", "farm_hub_id": farm_hub_id, "enterprise_id": tenant.enterprise_id} @router.get("/metrics") def sync_metrics( enterprise_id: str = Query(...), tenant: TenantContext = Depends(require_enterprise_zootech), ): if tenant.enterprise_id != enterprise_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN") return service.get_sync_metrics(enterprise_id) @router.get("/capabilities", response_model=SyncCapabilitiesResponse) def capabilities() -> SyncCapabilitiesResponse: return service.get_capabilities() @router.post("/changes/push", response_model=PushChangesResponse) def push_changes(body: PushChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PushChangesResponse: return service.push_changes(hub, body) @router.post("/changes/pull", response_model=PullChangesResponse) def pull_changes(body: PullChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PullChangesResponse: return service.pull_changes(hub, body) @router.post("/changes/ack", response_model=AckChangesResponse) def ack_changes(body: AckChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> AckChangesResponse: return service.ack_changes(hub, body) @router.post("/hubs/heartbeat", response_model=HeartbeatResponse) def heartbeat(body: HeartbeatRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> HeartbeatResponse: return service.hub_heartbeat(hub, body) @router.get("/hub/conflicts") def list_hub_conflicts(hub: HubPrincipal = Depends(get_hub_auth)): return service.list_conflicts_for_enterprise(hub.enterprise_id) @router.get("/hub/conflicts/{conflict_id}") def get_hub_conflict(conflict_id: str, hub: HubPrincipal = Depends(get_hub_auth)): try: return service.get_conflict_detail(conflict_id, hub.enterprise_id) except SyncServiceError as exc: raise _map_error(exc) from exc @router.post("/hub/conflicts/{conflict_id}/resolve") def resolve_hub_conflict( conflict_id: str, body: ResolveConflictRequest, hub: HubPrincipal = Depends(get_hub_auth), ): try: service.resolve_conflict(conflict_id, hub.enterprise_id, f"hub:{hub.hub_site_id}", body) return {"status": "ok"} except SyncServiceError as exc: raise _map_error(exc) from exc @router.get("/conflicts") def list_conflicts( enterprise_id: str = Query(...), tenant: TenantContext = Depends(require_enterprise_zootech), ): if tenant.enterprise_id != enterprise_id and tenant.enterprise_role != "admin": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN") return service.list_conflicts_for_enterprise(enterprise_id) @router.get("/conflicts/{conflict_id}") def get_conflict( conflict_id: str, enterprise_id: str = Query(...), tenant: TenantContext = Depends(require_enterprise_zootech), ): try: return service.get_conflict_detail(conflict_id, enterprise_id) except SyncServiceError as exc: raise _map_error(exc) from exc @router.post("/conflicts/{conflict_id}/resolve") def resolve_conflict( conflict_id: str, body: ResolveConflictRequest, enterprise_id: str = Query(...), tenant: TenantContext = Depends(require_enterprise_zootech), ): try: service.resolve_conflict(conflict_id, enterprise_id, tenant.user_id or "", body) return {"status": "ok"} except SyncServiceError as exc: raise _map_error(exc) from exc enterprise_router = APIRouter() @enterprise_router.post("/pair/start") def pair_start(body: PairStartRequest, user: User = Depends(get_current_user)): try: return service.start_pairing(user, body) except SyncServiceError as exc: raise _map_error(exc) from exc @enterprise_router.post("/pair/confirm", response_model=PairConfirmResponse) def pair_confirm(body: PairConfirmRequest) -> PairConfirmResponse: try: return service.confirm_pairing(body) except SyncServiceError as exc: raise _map_error(exc) from exc