import logging import uuid from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from starlette.responses import Response from app.api.deps import get_current_user from app.database import get_db from app.models.brand import Brand from app.models.user import User from app.schemas.scoring import CitationResult from app.services.citation.citation import export_citations_csv, export_citations_pdf from app.services.scoring.scoring_service import ScoringService, ScoringResultV2 logger = logging.getLogger(__name__) router = APIRouter() def _to_uuid(value: str | uuid.UUID) -> uuid.UUID: if isinstance(value, uuid.UUID): return value return uuid.UUID(str(value)) async def _compute_v2_scores( db: AsyncSession, user_id: uuid.UUID, brand_id: uuid.UUID, ) -> ScoringResultV2 | None: try: from app.api.scoring import ( _get_citations_for_brand, _analyze_sentiments_for_citations, ) total_queries, brand_citations, _, competitor_mentions = ( await _get_citations_for_brand(db, user_id, brand_id) ) if total_queries == 0: return None brand_stmt = select(Brand).where( Brand.id == brand_id, Brand.user_id == user_id ) brand_result = await db.execute(brand_stmt) brand = brand_result.scalar_one_or_none() if not brand: return None sentiment_counts = await _analyze_sentiments_for_citations( brand_name=brand.name, brand_citations=brand_citations, ) citation_results = [ CitationResult( cited=c.cited, position=c.citation_position, citation_text=c.citation_text, sentiment=c.sentiment or "neutral", confidence=c.confidence or 0.0, ) for c in brand_citations ] positions = [c.citation_position for c in brand_citations if c.cited] scoring_service = ScoringService() return scoring_service.calculate_v2( mentioned_count=len(brand_citations), total_queries=total_queries, positions=positions, sentiment_counts=sentiment_counts, citations=citation_results, brand_mentions=len(brand_citations), competitor_mentions=competitor_mentions, ) except Exception: logger.warning("V2 scoring failed for brand %s", brand_id, exc_info=True) return None @router.get("/export/csv") async def export_report( query_id: uuid.UUID = Query(...), brand_id: Optional[uuid.UUID] = Query(None), format: str = Query("csv"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): if format != "csv": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only CSV format is supported", ) try: v2_result = None if brand_id is not None: v2_result = await _compute_v2_scores(db, _to_uuid(current_user.id), brand_id) csv_content = await export_citations_csv( db, _to_uuid(current_user.id), query_id, v2_result=v2_result ) except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) date_str = datetime.now().strftime("%Y%m%d") filename = f"geo-report-{date_str}.csv" return StreamingResponse( iter([csv_content]), media_type="text/csv", headers={ "Content-Disposition": f'attachment; filename="{filename}"', }, ) @router.get("/export/pdf") async def export_pdf( query_id: Optional[uuid.UUID] = None, brand_id: Optional[uuid.UUID] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: v2_result = None if brand_id is not None: v2_result = await _compute_v2_scores(db, _to_uuid(current_user.id), brand_id) pdf_bytes = await export_citations_pdf( db, _to_uuid(current_user.id), query_id, v2_result=v2_result ) except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) date_str = datetime.now().strftime("%Y%m%d") filename = f"geo-report-{date_str}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="{filename}"', }, )