from __future__ import annotations from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope from govoplan_core.core.institutional import InstitutionalContextError from govoplan_core.db.session import get_session from govoplan_forms.backend.manifest import ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE from govoplan_forms.backend.schemas import ( FormDefinitionHistoryResponse, FormDefinitionListResponse, FormDefinitionWriteRequest, FormPackageImportRequest, FormPackageRequest, ) from govoplan_forms.backend.service import ( FormDefinitionStoreError, definition_from_mapping, assess_form_definition_fragment, export_form_definition_fragment, form_definition_diagnostics, form_definition_history, get_form_definition, list_form_definitions, import_form_definition_fragment, record_form_definition, ) router = APIRouter(prefix="/forms", tags=["forms"]) def _require(principal: ApiPrincipal, scope: str) -> None: if not has_scope(principal, scope): raise HTTPException(status_code=403, detail=f"Missing scope: {scope}") def _error(exc: Exception) -> HTTPException: message = str(exc) code = ( 409 if any(word in message.casefold() for word in ("conflict", "already", "stale")) else 400 ) return HTTPException(status_code=code, detail=message) @router.get("/definitions", response_model=FormDefinitionListResponse) def api_list_form_definitions( q: str = Query(default="", max_length=200), publication_state: list[str] | None = Query(default=None), offset: int = Query(default=0, ge=0), limit: int = Query(default=100, ge=1, le=200), session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> FormDefinitionListResponse: _require(principal, READ_SCOPE) try: items, total = list_form_definitions( session, principal, query=q, publication_states=publication_state, offset=offset, limit=limit, ) except FormDefinitionStoreError as exc: raise _error(exc) from exc return FormDefinitionListResponse( definitions=[item.to_dict() for item in items], total=total, ) @router.put( "/definitions/{form_id}", response_model=dict[str, object], status_code=status.HTTP_200_OK, ) def api_record_form_definition( form_id: str, payload: FormDefinitionWriteRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, WRITE_SCOPE) try: definition = definition_from_mapping(payload.definition) if definition.reference.object_id != form_id: raise FormDefinitionStoreError( "Form definition path and payload IDs must match." ) if definition.publication_state != "draft": _require(principal, ADMIN_SCOPE) stored = record_form_definition( session, principal, definition=definition, expected_revision=payload.expected_revision, ) session.commit() except (FormDefinitionStoreError, InstitutionalContextError) as exc: session.rollback() raise _error(exc) from exc return stored.to_dict() @router.get("/definitions/{form_id}", response_model=dict[str, object]) def api_get_form_definition( form_id: str, revision: str | None = Query(default=None, max_length=255), session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, READ_SCOPE) item = get_form_definition( session, principal, form_id=form_id, revision=revision, ) if item is None: raise HTTPException(status_code=404, detail="Form definition not found") return item.to_dict() @router.get("/definitions/{form_id}/diagnostics", response_model=dict[str, object]) def api_form_definition_diagnostics( form_id: str, revision: str | None = Query(default=None, max_length=255), session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, READ_SCOPE) item = get_form_definition(session, principal, form_id=form_id, revision=revision) if item is None: raise HTTPException(status_code=404, detail="Form definition not found") diagnostics = form_definition_diagnostics(item) return { "diagnostics": [dict(value) for value in diagnostics], "error_count": sum(value.get("severity") == "error" for value in diagnostics), "warning_count": sum( value.get("severity") == "warning" for value in diagnostics ), } @router.get("/definitions/{form_id}/package", response_model=dict[str, object]) def api_export_form_definition_package( form_id: str, revision: str | None = Query(default=None, max_length=255), session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, READ_SCOPE) item = get_form_definition(session, principal, form_id=form_id, revision=revision) if item is None: raise HTTPException(status_code=404, detail="Form definition not found") actor_id = next( ( str(getattr(principal, name)) for name in ("account_id", "identity_id", "membership_id") if getattr(principal, name, None) ), None, ) return export_form_definition_fragment( item, exported_at=datetime.now(UTC), exported_by=actor_id, ) @router.post("/packages/assess", response_model=dict[str, object]) def api_assess_form_definition_package( payload: FormPackageRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, WRITE_SCOPE) try: return assess_form_definition_fragment( session, principal, fragment=payload.fragment, ) except (FormDefinitionStoreError, InstitutionalContextError) as exc: raise _error(exc) from exc @router.post("/packages/import", response_model=dict[str, object]) def api_import_form_definition_package( payload: FormPackageImportRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> dict[str, object]: _require(principal, WRITE_SCOPE) try: item = import_form_definition_fragment( session, principal, fragment=payload.fragment, target_form_id=payload.target_form_id, target_key=payload.target_key, expected_revision=payload.expected_revision, change_reason=payload.change_reason, recorded_at=payload.recorded_at, ) session.commit() except (FormDefinitionStoreError, InstitutionalContextError) as exc: session.rollback() raise _error(exc) from exc return item.to_dict() @router.get( "/definitions/{form_id}/history", response_model=FormDefinitionHistoryResponse, ) def api_form_definition_history( form_id: str, limit: int = Query(default=100, ge=1, le=200), session: Session = Depends(get_session), principal: ApiPrincipal = Depends(get_api_principal), ) -> FormDefinitionHistoryResponse: _require(principal, READ_SCOPE) return FormDefinitionHistoryResponse( revisions=[ item.to_dict() for item in form_definition_history( session, principal, form_id=form_id, limit=limit, ) ] ) __all__ = ["router"]