from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app.core.database import get_db from app.core.dependencies import CurrentUser from app.core.logging_config import get_logger from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository from app.repositories.template_repository import ( TemplateRepository, DocumentRegionRepository, DocumentCellRepository, TableFormatRepository, TableColumnRepository ) from app.schemas.common import PaginatedResponse, SuccessResponse from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse from app.schemas.template import ( TemplateListResponse, TemplateRenderRequest, TemplateRenderResponse, TemplateResponse, TemplateCreateRequest, TemplateMappingSaveRequest, ) logger = get_logger(__name__) router = APIRouter(prefix="/templates", tags=["Templates"]) @router.post( "", response_model=dict, summary="Create Template", description="Create a new template.", ) def create_template( payload: TemplateCreateRequest, current_user: CurrentUser, db: Session = Depends(get_db), ) -> dict: """Create a template.""" template_repo = TemplateRepository(db) template = template_repo.create_template( name=payload.template_name, page_width=1000.0, page_height=1000.0, source_document_id=uuid.UUID(payload.source_document_id) if payload.source_document_id else None ) cell_repo = DocumentCellRepository(db) saved_fields = [] for index, field in enumerate(payload.fields): cell = cell_repo.create_cell( format_id=template.id, page_number=1, x=0.0, y=0.0, width=0.0, height=0.0, data_type=field.field_type, field_name=field.field_label, sequence=field.display_order or index, is_dynamic=True ) saved_fields.append({ "field_label": cell.field_name, "field_type": cell.data_type }) db.commit() return { "pk_template_id": str(template.id), "template_name": template.name, "fields": saved_fields } @router.post( "/{template_id}/mappings/save", response_model=SuccessResponse, summary="Save Template Mappings", description="Save the field mappings for a template.", ) def save_mappings( template_id: uuid.UUID, payload: list[TemplateMappingSaveRequest], current_user: CurrentUser, db: Session = Depends(get_db), ) -> SuccessResponse: """Save template mappings.""" template_repo = TemplateRepository(db) template = template_repo.get_by_id(template_id) if not template: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{template_id}' not found", ) region_repo = DocumentRegionRepository(db) cell_repo = DocumentCellRepository(db) table_repo = TableFormatRepository(db) table_col_repo = TableColumnRepository(db) cells = cell_repo.get_template_cells(template_id) cell_map = {c.field_name: c for c in cells} table_format = None # Save each mapped node for mapping in payload: cell = cell_map.get(mapping.field_name) is_table_column = cell and cell.data_type == 'TABLE_COLUMN' for node_idx, node in enumerate(mapping.mapped_nodes): if is_table_column: if not table_format: table_format = table_repo.create_table_format( format_id=template_id, page_number=node.page_no or 1, x=0.0, y=0.0, width=1000.0, height=1000.0, rows=1, columns=10 ) table_col_repo.create_column( table_format_id=table_format.id, column_index=node_idx, width=node.width, header_text=mapping.field_name, data_type="text" ) region_repo.create_region( format_id=template_id, page_number=node.page_no or 1, region_type="field_mapping", x=node.x_coordinate, y=node.y_coordinate, width=node.width, height=node.height, content={ "field_name": mapping.field_name, "text_value": node.text_value } ) db.commit() # Generate fingerprint now that mappings are populated from app.services.fingerprint_service import FingerprintService FingerprintService(db).generate_fingerprint(template) db.commit() return SuccessResponse(message="Mappings saved successfully") @router.get( "", response_model=PaginatedResponse[TemplateListResponse], summary="List Templates", description="List all active templates with pagination.", ) def list_templates( current_user: CurrentUser, page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), db: Session = Depends(get_db), ) -> PaginatedResponse[TemplateListResponse]: """List all active templates.""" template_repo = TemplateRepository(db) offset = (page - 1) * page_size templates = template_repo.get_active_templates(offset=offset, limit=page_size) total = template_repo.count_active() items = [TemplateListResponse.model_validate(t) for t in templates] return PaginatedResponse.create( items=items, total=total, page=page, page_size=page_size, ) @router.get( "/{template_id}", response_model=TemplateResponse, summary="Get Template", description="Retrieve a template by ID with all its components.", ) def get_template( template_id: uuid.UUID, current_user: CurrentUser, db: Session = Depends(get_db), ) -> TemplateResponse: """Get a template by ID.""" template_repo = TemplateRepository(db) template = template_repo.get_by_id(template_id) if not template: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{template_id}' not found", ) return TemplateResponse.model_validate(template) @router.delete( "/{template_id}", response_model=SuccessResponse, summary="Delete Template", description="Soft-delete a template by deactivating it.", ) def delete_template( template_id: uuid.UUID, current_user: CurrentUser, db: Session = Depends(get_db), ) -> SuccessResponse: """Soft-delete a template.""" template_repo = TemplateRepository(db) template = template_repo.get_by_id(template_id) if not template: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{template_id}' not found", ) template_repo.deactivate_template(template_id) db.commit() return SuccessResponse(message=f"Template '{template_id}' deactivated successfully") @router.post( "/match", response_model=list[TemplateMatchResponse], summary="Match Document to Templates", description="Match a document against existing templates and return ranked results.", ) def match_template( payload: TemplateMatchRequest, current_user: CurrentUser, db: Session = Depends(get_db), ) -> list[TemplateMatchResponse]: """Match a document against existing templates.""" doc_repo = DocumentRepository(db) document = doc_repo.get_by_id(payload.document_id) if not document: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Document '{payload.document_id}' not found", ) if document.status != "completed": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Document must be in 'completed' status. Current status: '{document.status}'", ) # Perform template matching from app.services.matching_service import MatchingService matching_service = MatchingService(db) matches = matching_service.match_document( document_id=payload.document_id, min_confidence=payload.min_confidence, max_results=payload.max_results, ) db.commit() results = [] for match in matches: resp = TemplateMatchResponse( id=match.id, document_id=match.document_id, format_id=match.format_id, confidence_score=match.confidence_score, match_details=match.match_details, selected=match.selected, template_name=match.template.name if match.template else None, created_at=match.created_at, ) results.append(resp) return results @router.post( "/render", response_model=TemplateRenderResponse, summary="Render Template to PDF", description="Generate a PDF from a stored template with supplied data.", ) def render_template( payload: TemplateRenderRequest, current_user: CurrentUser, db: Session = Depends(get_db), ) -> TemplateRenderResponse: """Render a template to PDF.""" template_repo = TemplateRepository(db) template = template_repo.get_by_id(payload.template_id) if not template: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{payload.template_id}' not found", ) if not template.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Template is deactivated", ) from app.services.reconstruction_service import ReconstructionService reconstruction_service = ReconstructionService(db) result = reconstruction_service.render_template( template=template, data=payload.data, output_filename=payload.output_filename, images=payload.images, ) return result @router.get( "/{template_id}/download", summary="Download Rendered PDF", description="Download a previously rendered PDF.", ) def download_rendered_pdf( template_id: uuid.UUID, current_user: CurrentUser, filename: str = Query(..., description="Filename of the rendered PDF"), db: Session = Depends(get_db), ) -> FileResponse: """Download a rendered PDF.""" from app.storage.provider import get_storage_provider storage = get_storage_provider() storage_path = f"rendered/{filename}" if not storage.file_exists(storage_path): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Rendered PDF '{filename}' not found", ) absolute_path = storage.get_absolute_path(storage_path) return FileResponse( path=absolute_path, media_type="application/pdf", filename=filename, )