from __future__ import annotations from dataclasses import asdict from datetime import datetime, timezone from uuid import uuid4 from app.domain.editor import EditorSession, VariantChange from app.domain.editor_builder import build_runtime_payload_from_changes from app.repositories.directus.variant_changes import VariantChangeRepository from app.repositories.native.editor_sessions import EditorSessionRepository from app.schemas.auth import AuthenticatedUser from app.schemas.editor import ( BuildPreviewRequest, BuildPreviewResponse, EditorChangeListResponse, EditorChangeRead, EditorSessionCreateRequest, EditorSessionRead, EditorSessionUpdateRequest, SaveVariantChangesRequest, ) class EditorService: """Application service for visual-editor workflows.""" def __init__( self, change_repository: VariantChangeRepository | None = None, session_repository: EditorSessionRepository | None = None, ) -> None: self.change_repository = change_repository or VariantChangeRepository() self.session_repository = session_repository or EditorSessionRepository() async def create_session( self, request: EditorSessionCreateRequest, current_user: AuthenticatedUser, ) -> EditorSessionRead: session = EditorSession( variant_id=request.variant_id, mode=request.mode, base_url=request.base_url, actor_id=current_user.id, actor_email=current_user.email, ) await self.session_repository.create(session) return EditorSessionRead.model_validate(asdict(session)) async def get_session(self, session_id: str) -> EditorSessionRead | None: session = await self.session_repository.get(session_id) if not session: return None return EditorSessionRead.model_validate(asdict(session)) async def update_session( self, session_id: str, request: EditorSessionUpdateRequest, ) -> EditorSessionRead | None: session = await self.session_repository.get(session_id) if not session: return None if request.status is not None: session.status = request.status if request.draft_changes is not None: session.draft_changes = request.draft_changes session.updated_at = datetime.now(timezone.utc) await self.session_repository.update(session) return EditorSessionRead.model_validate(asdict(session)) async def delete_session(self, session_id: str) -> None: await self.session_repository.delete(session_id) async def list_changes(self, variant_id: str) -> EditorChangeListResponse: items = await self.change_repository.list(params={"filter[variant_id][_eq]": variant_id}) mapped = [EditorChangeRead.model_validate(item) for item in items] return EditorChangeListResponse(items=mapped) async def list_changes_with_access_token( self, variant_id: str, access_token: str, ) -> EditorChangeListResponse: items = await self.change_repository.list( params={"filter[variant_id][_eq]": variant_id}, access_token=access_token, ) mapped = [EditorChangeRead.model_validate(item) for item in items] return EditorChangeListResponse(items=mapped) async def save_changes( self, variant_id: str, request: SaveVariantChangesRequest, ) -> EditorChangeListResponse: # Full-replace semantics: delete any existing changes not present in the request. existing = await self.change_repository.list(params={"filter[variant_id][_eq]": variant_id}) incoming_ids = {item.id for item in request.items if item.id} for existing_item in existing: if str(existing_item["id"]) not in incoming_ids: await self.change_repository.delete(str(existing_item["id"])) saved_items: list[EditorChangeRead] = [] for item in request.items: payload = item.model_dump() payload["variant_id"] = variant_id change_id = payload.pop("id", None) if change_id: saved = await self.change_repository.update(change_id, payload) else: payload["id"] = str(uuid4()) saved = await self.change_repository.create(payload) saved_items.append(EditorChangeRead.model_validate(saved)) return EditorChangeListResponse(items=saved_items) async def save_changes_with_access_token( self, variant_id: str, request: SaveVariantChangesRequest, access_token: str, ) -> EditorChangeListResponse: # Full-replace semantics: delete any existing changes not present in the request. existing = await self.change_repository.list( params={"filter[variant_id][_eq]": variant_id}, access_token=access_token, ) incoming_ids = {item.id for item in request.items if item.id} for existing_item in existing: if str(existing_item["id"]) not in incoming_ids: await self.change_repository.delete(str(existing_item["id"]), access_token=access_token) saved_items: list[EditorChangeRead] = [] for item in request.items: payload = item.model_dump() payload["variant_id"] = variant_id change_id = payload.pop("id", None) if change_id: saved = await self.change_repository.update(change_id, payload, access_token=access_token) else: payload["id"] = str(uuid4()) saved = await self.change_repository.create(payload, access_token=access_token) saved_items.append(EditorChangeRead.model_validate(saved)) return EditorChangeListResponse(items=saved_items) async def build_preview(self, request: BuildPreviewRequest) -> BuildPreviewResponse: changes = [ VariantChange( id=item.id or str(uuid4()), variant_id=request.variant_id, change_type=item.change_type, selector_type=item.selector_type, selector_value=item.selector_value, sort_order=item.sort_order, payload=item.payload, ) for item in request.items ] preview = build_runtime_payload_from_changes(request.variant_id, changes) return BuildPreviewResponse( variant_id=preview.variant_id, generated_at=preview.generated_at, operations=[asdict(operation) for operation in preview.operations], )