from datetime import datetime, timezone from typing import Any, Dict, List, Optional from fastapi import APIRouter, Body, Header, HTTPException import requests from app.core.config import ( SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_TIMEOUT_SECONDS, VEHICLE_SELECT_FIELDS, supabase_configured, LOGGER, SUPABASE_ANON_KEY, ) from app.security.auth import ( require_authenticated_user, _supabase_headers, _normalize_spaces, ) from app.api.helpers import _resolve_otp_identifier, _verify_supabase_otp, _normalize_otp_channel from app.services.supabase_client import supabase_get_profile_by_user_id router = APIRouter(prefix="/vehicles", tags=["vehicles"]) def _extract_vehicle_payload(payload: Dict[str, Any], *, require_plate: bool) -> Dict[str, str]: def _pick(*keys: str) -> Optional[str]: for key in keys: value = payload.get(key) if value is None: continue normalized_value = _normalize_spaces(str(value)) if normalized_value: return normalized_value return None plate_letters_ar = _pick("plate_letters_ar", "plate_letters") plate_numbers_ar = _pick("plate_numbers_ar", "plate_numbers") plate_letters_en = _pick("plate_letters_en") plate_numbers_en = _pick("plate_numbers_en") car_name = _pick("car_name", "nickname") car_model = _pick("car_model", "model") car_color = _pick("car_color", "color") payload_out: Dict[str, str] = {} if plate_letters_ar: payload_out["plate_letters_ar"] = plate_letters_ar if plate_numbers_ar: payload_out["plate_numbers_ar"] = plate_numbers_ar if plate_letters_en: payload_out["plate_letters_en"] = plate_letters_en if plate_numbers_en: payload_out["plate_numbers_en"] = plate_numbers_en if car_name: payload_out["car_name"] = car_name if car_model: payload_out["car_model"] = car_model if car_color: payload_out["car_color"] = car_color if require_plate and (not plate_letters_ar or not plate_numbers_ar): raise HTTPException(status_code=400, detail="plate_letters_ar and plate_numbers_ar are required.") if not payload_out: raise HTTPException(status_code=400, detail="No vehicle fields were provided.") return payload_out def _create_vehicle_for_owner(owner_id: str, vehicle_payload: Dict[str, str]) -> Optional[Dict[str, Any]]: if not owner_id or not vehicle_payload: return None payload: Dict[str, Any] = {"owner_id": owner_id, "plate_letters_ar": vehicle_payload["plate_letters_ar"], "plate_numbers_ar": vehicle_payload["plate_numbers_ar"]} for opt in ["plate_letters_en", "plate_numbers_en", "car_name", "car_model", "car_color"]: if vehicle_payload.get(opt): payload[opt] = vehicle_payload[opt] response = requests.post( f"{SUPABASE_URL}/rest/v1/vehicles", json=payload, 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, 201}: raise HTTPException(status_code=400, detail=f"Failed to save vehicle: {response.text[:260]}") rows = response.json() if isinstance(rows, list) and rows and isinstance(rows[0], dict): return rows[0] return None def _fetch_vehicle_for_owner(owner_id: str, vehicle_id: str) -> Optional[Dict[str, Any]]: response = requests.get( f"{SUPABASE_URL}/rest/v1/vehicles", params={"select": VEHICLE_SELECT_FIELDS, "id": f"eq.{vehicle_id}", "owner_id": f"eq.{owner_id}", "limit": "1"}, headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code != 200: raise HTTPException(status_code=502, detail=f"Failed to fetch vehicle: {response.text[:260]}") rows = response.json() if isinstance(rows, list) and rows and isinstance(rows[0], dict): return rows[0] return None def _list_vehicles_for_owner(owner_id: str) -> List[Dict[str, Any]]: if not owner_id: return [] response = requests.get( f"{SUPABASE_URL}/rest/v1/vehicles", params={"select": VEHICLE_SELECT_FIELDS, "owner_id": f"eq.{owner_id}", "order": "created_at.desc"}, headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code != 200: raise HTTPException(status_code=502, detail=f"Failed to list vehicles: {response.text[:260]}") rows = response.json() if not isinstance(rows, list): return [] return [row for row in rows if isinstance(row, dict)] def _update_vehicle_for_owner(owner_id: str, vehicle_id: str, update_payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: response = requests.patch( f"{SUPABASE_URL}/rest/v1/vehicles", params={"id": f"eq.{vehicle_id}", "owner_id": f"eq.{owner_id}", "select": VEHICLE_SELECT_FIELDS}, json=update_payload, 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 vehicle: {response.text[:260]}") if not response.text: return _fetch_vehicle_for_owner(owner_id, vehicle_id) rows = response.json() if isinstance(rows, list) and rows and isinstance(rows[0], dict): return rows[0] return _fetch_vehicle_for_owner(owner_id, vehicle_id) def _delete_vehicle_for_owner(owner_id: str, vehicle_id: str) -> bool: response = requests.delete( f"{SUPABASE_URL}/rest/v1/vehicles", params={"id": f"eq.{vehicle_id}", "owner_id": f"eq.{owner_id}", "select": "id"}, headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY, prefer="return=representation"), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code not in {200, 204}: raise HTTPException(status_code=400, detail=f"Failed to delete vehicle: {response.text[:260]}") if not response.text: return True rows = response.json() return isinstance(rows, list) and len(rows) > 0 def _fetch_vehicle_by_plate_for_owner(owner_id: str, plate_letters_ar: str, plate_numbers_ar: str) -> Optional[Dict[str, Any]]: response = requests.get( f"{SUPABASE_URL}/rest/v1/vehicles", params={"select": VEHICLE_SELECT_FIELDS, "owner_id": f"eq.{owner_id}", "plate_letters_ar": f"eq.{plate_letters_ar}", "plate_numbers_ar": f"eq.{plate_numbers_ar}", "limit": "1"}, headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code != 200: raise HTTPException(status_code=502, detail=f"Failed to fetch vehicle by plate: {response.text[:260]}") rows = response.json() if isinstance(rows, list) and rows and isinstance(rows[0], dict): return rows[0] return None @router.post("/add") def vehicles_add(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.") vehicle_payload = _extract_vehicle_payload(payload, require_plate=True) created_vehicle = _create_vehicle_for_owner(requester_id, vehicle_payload) if not isinstance(created_vehicle, dict): raise HTTPException(status_code=502, detail="Vehicle was not created.") return {"status": "ok", "vehicle": created_vehicle} @router.get("/my") def vehicles_my(include_inactive: bool = False, authorization: Optional[str] = Header(default=None)) -> Dict[str, Any]: 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.") rows = _list_vehicles_for_owner(requester_id) if not include_inactive: rows = [row for row in rows if bool(row.get("is_active", True))] return {"status": "ok", "count": len(rows), "vehicles": rows} @router.put("/{vehicle_id}") def vehicles_update(vehicle_id: str, payload: Dict[str, Any] = Body(...), authorization: Optional[str] = Header(default=None)) -> Dict[str, Any]: 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() update_payload: Dict[str, Any] = {} try: update_payload.update(_extract_vehicle_payload(payload, require_plate=False)) except HTTPException as exc: if str(exc.detail) != "No vehicle fields were provided.": raise if "is_active" in payload: raw = payload.get("is_active") if isinstance(raw, bool): update_payload["is_active"] = raw elif raw is not None: update_payload["is_active"] = str(raw).strip().lower() in {"1", "true", "yes", "on"} if not update_payload: raise HTTPException(status_code=400, detail="No updatable vehicle fields were provided.") updated_vehicle = _update_vehicle_for_owner(requester_id, vehicle_id, update_payload) if not isinstance(updated_vehicle, dict): raise HTTPException(status_code=404, detail="Vehicle was not found.") return {"status": "ok", "vehicle": updated_vehicle} @router.delete("/{vehicle_id}") def vehicles_delete(vehicle_id: str, authorization: Optional[str] = Header(default=None)) -> Dict[str, Any]: 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() vehicle_record = _fetch_vehicle_for_owner(requester_id, vehicle_id) if not isinstance(vehicle_record, dict): raise HTTPException(status_code=404, detail="Vehicle was not found.") deleted = _delete_vehicle_for_owner(requester_id, vehicle_id) if not deleted: raise HTTPException(status_code=404, detail="Vehicle was not found.") return {"status": "ok", "deleted_vehicle_id": vehicle_id} @router.post("/verify") def vehicles_verify( payload: Dict[str, Any] = Body(...), authorization: Optional[str] = Header(default=None), ) -> Dict[str, Any]: 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.") profile = supabase_get_profile_by_user_id(requester_id) if not isinstance(profile, dict): raise HTTPException(status_code=404, detail="Profile was not found.") vehicle_id = _normalize_spaces(str(payload.get("vehicle_id") or "")) if vehicle_id: vehicle = _fetch_vehicle_for_owner(requester_id, vehicle_id) else: vehicle_plate_payload = _extract_vehicle_payload(payload, require_plate=True) vehicle = _fetch_vehicle_by_plate_for_owner( requester_id, vehicle_plate_payload["plate_letters_ar"], vehicle_plate_payload["plate_numbers_ar"], ) if not isinstance(vehicle, dict): raise HTTPException(status_code=404, detail="Vehicle was not found.") channel = _normalize_otp_channel(str(payload.get("channel") or payload.get("otp_channel") or "email")) identifier = _resolve_otp_identifier(channel=channel, payload=payload, profile=profile) _verify_supabase_otp( channel=channel, identifier=identifier, token=str(payload.get("otp_token") or payload.get("token") or ""), verify_type=str(payload.get("type") or payload.get("verify_type") or "").strip() or None, purpose="vehicle_verify", ) resolved_vehicle_id = str(vehicle.get("id") or "").strip() updated_vehicle = _update_vehicle_for_owner( requester_id, resolved_vehicle_id, { "is_verified": True, "verified_at": datetime.now(timezone.utc).isoformat(), "verified_by": requester_id, }, ) return { "status": "ok", "channel": channel, "vehicle": updated_vehicle or vehicle, "message": "Vehicle ownership verified.", }