"""Public survey submission endpoint. No authentication required.""" import logging from typing import Annotated from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.database import get_db from app.core.email import EmailService from app.core.rate_limit import limiter from app.models.survey_invite import SurveyInvite from app.models.survey_response import SurveyResponse from app.schemas.survey import SurveyEmailCopyRequest, SurveyInviteStatus, SurveySubmission, SurveySubmissionResponse logger = logging.getLogger(__name__) router = APIRouter(tags=["survey"]) @router.get("/survey/invite/{token}", response_model=SurveyInviteStatus) async def check_invite_status( token: str, db: Annotated[AsyncSession, Depends(get_db)], ): """Check if a survey invite token is valid and its status.""" result = await db.execute( select(SurveyInvite).where(SurveyInvite.token == token) ) invite = result.scalar_one_or_none() if not invite: raise HTTPException(status_code=404, detail="Invalid invite token") return SurveyInviteStatus(name=invite.recipient_name, status=invite.status) @router.post("/survey/submit", response_model=SurveySubmissionResponse) @limiter.limit("3/hour") async def submit_survey( request: Request, data: SurveySubmission, db: Annotated[AsyncSession, Depends(get_db)], ): """Accept a public survey submission. No auth required. Rate limited to 3/hour per IP to prevent spam. Saves to DB and sends notification email to configured address. """ ip = request.client.host if request.client else None ua = request.headers.get("user-agent", "") invite = None if data.token: result = await db.execute( select(SurveyInvite).where(SurveyInvite.token == data.token) ) invite = result.scalar_one_or_none() if invite and invite.status == "completed": raise HTTPException(status_code=409, detail="This survey has already been submitted") response = SurveyResponse( respondent_name=data.respondent_name or (invite.recipient_name if invite else None), responses=data.responses, ip_address=ip, user_agent=ua, invite_id=invite.id if invite else None, ) db.add(response) if invite: invite.status = "completed" invite.completed_at = datetime.now(timezone.utc) await db.flush() try: if settings.FEEDBACK_EMAIL: await EmailService.send_survey_notification_email( to_email=settings.FEEDBACK_EMAIL, respondent_name=response.respondent_name, responses=data.responses, ) except Exception: logger.exception("Failed to send survey notification email") await db.commit() return SurveySubmissionResponse(id=str(response.id)) # Question metadata for formatting email copy _QUESTION_LABELS = { "prereqs": "Q1. Before you start troubleshooting, what info do you need?", "verify_fix": "Q2. After you apply a fix, how do you verify it actually worked?", "steps_at_a_time": "Q3. How many steps do you prefer to see at once?", "first_step": "Q4. \"Internet is down.\" What's your FIRST move?", "junior_mistake": "Q5. Most common mistake junior engineers make?", "pivot": "Q6. When do you stop pursuing one theory and pivot?", "scenario_approach": "Q7. First 3 diagnostic steps for this ticket.", "scenario_deeper": "Q8. Server pings fine, you can RDP in. What next?", "doc_pct": "Q9. Percentage of steps you actually document?", "go_to_commands": "Q10. Top 3 go-to PowerShell commands?", "secret_weapon": "Q11. Secret weapon command/tool/technique?", "gotcha": "Q12. Issue where the obvious diagnosis was WRONG?", "hard_rules": "Q13. Which rules do you follow?", "prioritization": "Q14. Rank factors by diagnostic priority.", "detail_level": "Q15. How specific should AI suggestions be?", "ai_personality": "Q16. What makes an AI feel like a useful colleague?", } _QUESTION_ORDER = [ "prereqs", "verify_fix", "steps_at_a_time", "first_step", "junior_mistake", "pivot", "scenario_approach", "scenario_deeper", "doc_pct", "go_to_commands", "secret_weapon", "gotcha", "hard_rules", "prioritization", "detail_level", "ai_personality", ] @router.post("/survey/email-copy") @limiter.limit("5/hour") async def email_survey_copy( request: Request, data: SurveyEmailCopyRequest, db: Annotated[AsyncSession, Depends(get_db)], ): """Email a copy of survey responses to the respondent.""" from uuid import UUID as _UUID try: resp_id = _UUID(data.response_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid response ID") result = await db.execute( select(SurveyResponse).where(SurveyResponse.id == resp_id) ) response = result.scalar_one_or_none() if not response: raise HTTPException(status_code=404, detail="Response not found") # Build formatted responses for the email answers = response.responses or {} formatted_lines = [] for qid in _QUESTION_ORDER: label = _QUESTION_LABELS.get(qid, qid) val = answers.get(qid) if isinstance(val, list): answer_str = ", ".join(str(v) for v in val) elif val is not None: answer_str = str(val) else: answer_str = "(no answer)" formatted_lines.append(f"{label}\n{answer_str}") try: await EmailService.send_survey_copy_email( to_email=data.email, respondent_name=response.respondent_name, formatted_responses="\n\n".join(formatted_lines), ) except Exception: logger.exception("Failed to send survey copy email to %s", data.email) raise HTTPException(status_code=502, detail="Failed to send email. Please try again.") return {"message": "Email sent"}