"""API endpoints for tree markdown import/export.""" from typing import Annotated from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.orm import selectinload from app.core.database import get_db from app.models.tree import Tree from app.models.user import User from app.api.deps import get_current_active_user, require_engineer_or_admin from app.core.permissions import can_access_tree, can_edit_tree from app.schemas.tree_markdown import ( TreeMarkdownExportResponse, TreeMarkdownImportRequest, TreeMarkdownValidationResponse, MarkdownValidationError, ) from app.services.tree_markdown_service import serialize_tree_to_markdown from app.services.tree_markdown_parser import parse_markdown_to_tree from app.services.tree_markdown_validator import validate_tree_markdown router = APIRouter(prefix="/trees", tags=["tree-markdown"]) @router.get("/{tree_id}/export-markdown", response_model=TreeMarkdownExportResponse) async def export_tree_markdown( tree_id: UUID, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_active_user)], ): """Export a tree's JSONB structure as ResolutionFlow markdown.""" result = await db.execute( select(Tree).where(Tree.id == tree_id, Tree.deleted_at.is_(None)) ) tree = result.scalar_one_or_none() if not tree: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found") if not can_access_tree(current_user, tree): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied") metadata = { "name": tree.name or "", "description": tree.description or "", "category": tree.category or "", } markdown = serialize_tree_to_markdown(tree.tree_structure, metadata=metadata) return TreeMarkdownExportResponse(markdown=markdown) @router.put("/{tree_id}/import-markdown", response_model=TreeMarkdownValidationResponse) async def import_tree_markdown( tree_id: UUID, body: TreeMarkdownImportRequest, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_engineer_or_admin)], ): """Parse markdown and update a tree's JSONB structure.""" result = await db.execute( select(Tree).where(Tree.id == tree_id, Tree.deleted_at.is_(None)) ) tree = result.scalar_one_or_none() if not tree: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found") if not can_edit_tree(current_user, tree): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied") parse_result = parse_markdown_to_tree(body.markdown) has_errors = any(e.severity == "error" for e in parse_result.errors) if has_errors: return TreeMarkdownValidationResponse( valid=False, errors=[ MarkdownValidationError( line=e.line, column=e.column, message=e.message, severity=e.severity ) for e in parse_result.errors ], tree_structure=None, ) # Apply the parsed tree structure tree.tree_structure = parse_result.tree_structure await db.commit() return TreeMarkdownValidationResponse( valid=True, errors=[ MarkdownValidationError( line=e.line, column=e.column, message=e.message, severity=e.severity ) for e in parse_result.errors ], tree_structure=parse_result.tree_structure, ) @router.post("/validate-markdown", response_model=TreeMarkdownValidationResponse) async def validate_markdown( body: TreeMarkdownImportRequest, current_user: Annotated[User, Depends(get_current_active_user)], ): """Validate markdown without saving. Returns errors and preview JSONB.""" parse_result = parse_markdown_to_tree(body.markdown) all_errors = validate_tree_markdown(body.markdown) # Deduplicate errors by message seen: set[str] = set() unique_errors: list[MarkdownValidationError] = [] for e in all_errors: key = f"{e.line}:{e.column}:{e.message}" if key not in seen: seen.add(key) unique_errors.append( MarkdownValidationError( line=e.line, column=e.column, message=e.message, severity=e.severity ) ) has_hard_errors = any(e.severity == "error" for e in unique_errors) return TreeMarkdownValidationResponse( valid=not has_hard_errors, errors=unique_errors, tree_structure=parse_result.tree_structure, metadata=parse_result.metadata, )