from datetime import datetime, timezone from typing import Any, Dict, Optional from fastapi import APIRouter, Body, Header, HTTPException import requests from app.core.config import ( SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_TIMEOUT_SECONDS, PHONE_PATTERN, PROFILE_SELECT_FIELDS, supabase_configured, LOGGER, ) from app.security.auth import require_authenticated_user, _supabase_headers, _normalize_spaces from app.services.supabase_client import supabase_get_profile_by_user_id, supabase_get_profile_by_field from app.api.helpers import ( _resolve_profile_name_fields, _compose_full_name, _missing_schema_hint, ) router = APIRouter(prefix="/profile", tags=["profile"]) @router.put("/update") def profile_update( payload: Dict[str, Any] = Body(...), authorization: Optional[str] = Header(default=None), ) -> Dict[str, Any]: if not supabase_configured(): raise HTTPException(status_code=503, detail="Supabase is not configured.") request_user = require_authenticated_user(authorization) if request_user is None: raise HTTPException(status_code=401, detail="Authentication is required.") requester_id = str(request_user.get("id") or "").strip() if not requester_id: raise HTTPException(status_code=401, detail="Authenticated user id is missing.") current_profile = supabase_get_profile_by_user_id(requester_id) or {} current_first_name, current_last_name, _ = _resolve_profile_name_fields( current_profile if isinstance(current_profile, dict) else None, fallback_full_name=(current_profile or {}).get("full_name") if isinstance(current_profile, dict) else None, ) name_fields_provided = any(key in payload for key in ["first_name", "firstName", "last_name", "lastName", "full_name", "name"]) requested_first_name, requested_last_name, _ = _resolve_profile_name_fields(payload) email = _normalize_spaces(str(payload.get("email") or "")).lower() phone_number = _normalize_spaces(str(payload.get("phone") or payload.get("phone_number") or "")) profile_patch: Dict[str, Any] = {} if name_fields_provided: next_first_name = requested_first_name or current_first_name next_last_name = requested_last_name or current_last_name next_full_name = _compose_full_name(next_first_name, next_last_name) if not next_full_name: raise HTTPException(status_code=400, detail="Name update requires at least first_name or full_name.") profile_patch["first_name"] = next_first_name profile_patch["last_name"] = next_last_name profile_patch["full_name"] = next_full_name if email: if "@" not in email: raise HTTPException(status_code=400, detail="email format is invalid.") other_with_email = supabase_get_profile_by_field("email", email, case_insensitive=True) if isinstance(other_with_email, dict) and str(other_with_email.get("id") or "") != requester_id: raise HTTPException(status_code=409, detail="email is already in use.") profile_patch["email"] = email if phone_number: if not PHONE_PATTERN.fullmatch(phone_number): raise HTTPException(status_code=400, detail="phone must be 7-15 digits and may start with +.") other_with_phone = supabase_get_profile_by_field("phone_number", phone_number) if isinstance(other_with_phone, dict) and str(other_with_phone.get("id") or "") != requester_id: raise HTTPException(status_code=409, detail="phone is already in use.") profile_patch["phone_number"] = phone_number if not profile_patch: raise HTTPException(status_code=400, detail="No profile fields were provided.") response = requests.patch( f"{SUPABASE_URL}/rest/v1/profiles", params={"id": f"eq.{requester_id}", "select": PROFILE_SELECT_FIELDS}, json=profile_patch, headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY, content_type="application/json", prefer="return=representation"), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code not in {200, 204}: raise HTTPException(status_code=400, detail=f"Failed to update profile: {_missing_schema_hint(response.text)}") updated_rows = response.json() if response.text else [] updated_profile = ( updated_rows[0] if isinstance(updated_rows, list) and updated_rows and isinstance(updated_rows[0], dict) else (supabase_get_profile_by_user_id(requester_id) or current_profile) ) auth_patch: Dict[str, Any] = {} user_metadata_patch: Dict[str, Any] = {} if "email" in profile_patch: auth_patch["email"] = profile_patch["email"] if "first_name" in profile_patch: user_metadata_patch["first_name"] = profile_patch["first_name"] if "last_name" in profile_patch: user_metadata_patch["last_name"] = profile_patch["last_name"] if "full_name" in profile_patch: user_metadata_patch["full_name"] = profile_patch["full_name"] if "phone_number" in profile_patch: user_metadata_patch["phone_number"] = profile_patch["phone_number"] user_metadata_patch["phone"] = profile_patch["phone_number"] if user_metadata_patch: auth_patch["user_metadata"] = user_metadata_patch if auth_patch: try: auth_response = requests.put( f"{SUPABASE_URL}/auth/v1/admin/users/{requester_id}", json=auth_patch, headers={"apikey": SUPABASE_SERVICE_ROLE_KEY, "Authorization": f"Bearer {SUPABASE_SERVICE_ROLE_KEY}", "Content-Type": "application/json"}, timeout=SUPABASE_TIMEOUT_SECONDS, ) if auth_response.status_code not in {200, 201}: LOGGER.warning("Auth metadata update failed for %s: %s", requester_id, auth_response.text[:220]) except requests.RequestException as exc: LOGGER.warning("Auth metadata update request failed for %s: %s", requester_id, exc) return {"status": "ok", "profile": updated_profile, "message": "Profile updated successfully."}