import hashlib import secrets import uuid from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, Request, Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth.deps import get_current_user from app.config import settings from app.database import get_db from app.middleware.rate_limit import limiter from app.models.contact_lead import ContactLead from app.models.member import Member, MagicLinkToken from app.models.user import User from app.schemas.contact import ( ContactLeadCreate, ContactLeadInviteRequest, ContactLeadInviteResponse, ContactLeadResponse, ContactLeadUpdate, ) from app.services.email import send_onboarding_invite router = APIRouter(tags=["Contact Leads"]) def _split_name(full_name: str) -> tuple[str, str]: parts = [part for part in full_name.strip().split() if part] if not parts: return "Goodwalk", "Client" if len(parts) == 1: return parts[0], "Client" return parts[0], " ".join(parts[1:]) def _normalise_services(payload: ContactLeadCreate) -> str | None: if payload.services: return ", ".join(payload.services) if payload.service: return payload.service.strip() or None return None @router.post("/api/contact", response_model=ContactLeadResponse, status_code=201) @limiter.limit("10/minute") async def submit_contact_lead( request: Request, response: Response, data: ContactLeadCreate, db: AsyncSession = Depends(get_db), ): lead = ContactLead( full_name=data.name.strip(), email=data.email.strip().lower(), phone=(data.phone or "").strip() or None, requested_services=_normalise_services(data), pet_name=(data.petName or "").strip() or None, pet_breed=(data.petBreed or "").strip() or None, suburb=(data.location or "").strip() or None, service_area_status=(data.serviceAreaStatus or "").strip() or None, message=(data.message or "").strip() or None, source=data.source, status="invite", metadata_json={ "services": data.services, "service": data.service, }, ) db.add(lead) await db.flush() await db.refresh(lead) return lead @router.get("/api/v1/admin/leads", response_model=list[ContactLeadResponse]) async def admin_list_leads( _admin: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(ContactLead).order_by(ContactLead.created_at.desc())) return result.scalars().all() @router.put("/api/v1/admin/leads/{lead_id}", response_model=ContactLeadResponse) async def admin_update_lead( lead_id: uuid.UUID, data: ContactLeadUpdate, _admin: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(ContactLead).where(ContactLead.id == lead_id)) lead = result.scalars().first() if lead is None: raise HTTPException(status_code=404, detail="Lead not found.") for field, value in data.model_dump(exclude_unset=True).items(): setattr(lead, field, value) await db.flush() await db.refresh(lead) return lead @router.post("/api/v1/admin/leads/{lead_id}/invite", response_model=ContactLeadInviteResponse) async def admin_invite_lead( lead_id: uuid.UUID, data: ContactLeadInviteRequest, _admin: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(ContactLead).where(ContactLead.id == lead_id)) lead = result.scalars().first() if lead is None: raise HTTPException(status_code=404, detail="Lead not found.") existing_member_result = await db.execute(select(Member).where(Member.email == lead.email)) member = existing_member_result.scalars().first() if member is None: first_name, last_name = _split_name(lead.full_name) member = Member( email=lead.email, first_name=first_name, last_name=last_name, phone=lead.phone, address=lead.suburb, onboarding_data={ "dog_name": lead.pet_name, "dog_breed": lead.pet_breed, "preferred_service": lead.requested_services, "lead_message": lead.message, "service_area_status": lead.service_area_status, "source": lead.source, }, is_claimed=False, is_active=True, member_status="invited", ) db.add(member) await db.flush() lead.invited_member_id = member.id lead.invited_at = datetime.now(timezone.utc) lead.status = "invited" await db.flush() await db.refresh(lead) await db.refresh(member) if data.send_email: plaintext_token = secrets.token_urlsafe(32) token_hash = hashlib.sha256(plaintext_token.encode()).hexdigest() magic_token = MagicLinkToken( member_id=member.id, token_hash=token_hash, expires_at=datetime.now(timezone.utc) + timedelta(days=7), ) db.add(magic_token) await db.flush() magic_url = f"{settings.MEMBERS_URL.rstrip('/')}/join?token={plaintext_token}" await send_onboarding_invite(lead.email, member.first_name, magic_url) return ContactLeadInviteResponse( lead=ContactLeadResponse.model_validate(lead), member_id=member.id, member_status=member.member_status, )