import { API_ENDPOINTS } from "@/lib/api_urls"; import { getStoredTokens } from "./auth"; import type { CreateAppointmentInput, ScheduleAppointmentInput, RejectAppointmentInput, UpdateAvailabilityInput, } from "@/lib/schema/appointments"; import type { Appointment, AppointmentResponse, AppointmentsListResponse, AvailableDatesResponse, AdminAvailability, AppointmentStats, UserAppointmentStats, JitsiMeetingInfo, ApiError, WeeklyAvailabilityResponse, AvailabilityConfig, CheckDateAvailabilityResponse, AvailabilityOverview, SelectedSlot, } from "@/lib/models/appointments"; // Helper function to extract error message from API response function extractErrorMessage(error: ApiError): string { if (error.detail) { if (Array.isArray(error.detail)) { return error.detail.join(", "); } return String(error.detail); } if (error.message) { if (Array.isArray(error.message)) { return error.message.join(", "); } return String(error.message); } if (typeof error === "string") { return error; } return "An error occurred while creating the appointment"; } // Create appointment export async function createAppointment( input: CreateAppointmentInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required. Please log in to book an appointment."); } // Validate required fields if (!input.first_name || !input.last_name || !input.email) { throw new Error("First name, last name, and email are required"); } // New API format: use selected_slots if (!input.selected_slots || input.selected_slots.length === 0) { throw new Error("At least one time slot must be selected"); } // Validate and clean selected_slots to ensure all have day and time_slot // This filters out any invalid slots and ensures proper format const validSlots: SelectedSlot[] = input.selected_slots .filter((slot, index) => { // Check if slot exists and is an object if (!slot || typeof slot !== 'object') { return false; } // Check if both day and time_slot properties exist if (typeof slot.day === 'undefined' || typeof slot.time_slot === 'undefined') { return false; } // Validate day is a number between 0-6 const dayNum = Number(slot.day); if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) { return false; } // Validate time_slot is a valid string (normalize to lowercase) const timeSlot = String(slot.time_slot).toLowerCase().trim(); if (!['morning', 'afternoon', 'evening'].includes(timeSlot)) { return false; } return true; }) .map(slot => ({ day: Number(slot.day), time_slot: String(slot.time_slot).toLowerCase().trim() as "morning" | "afternoon" | "evening", })); if (validSlots.length === 0) { throw new Error("At least one valid time slot must be selected. Each slot must have both 'day' (0-6) and 'time_slot' (morning, afternoon, or evening)."); } // Limit field lengths to prevent database errors (100 char limit for all string fields) // Truncate all string fields BEFORE trimming to handle edge cases const firstName = input.first_name ? String(input.first_name).trim().substring(0, 100) : ''; const lastName = input.last_name ? String(input.last_name).trim().substring(0, 100) : ''; const email = input.email ? String(input.email).trim().toLowerCase().substring(0, 100) : ''; const phone = input.phone ? String(input.phone).trim().substring(0, 100) : undefined; const reason = input.reason ? String(input.reason).trim().substring(0, 100) : undefined; // Build payload with only the fields the API expects - no extra fields const payload: { first_name: string; last_name: string; email: string; selected_slots: Array<{ day: number; time_slot: string }>; phone?: string; reason?: string; } = { first_name: firstName, last_name: lastName, email: email, selected_slots: validSlots.map(slot => ({ day: Number(slot.day), time_slot: String(slot.time_slot).toLowerCase().trim(), })), }; // Only add optional fields if they have values (and are within length limits) if (phone && phone.length > 0 && phone.length <= 100) { payload.phone = phone; } if (reason && reason.length > 0 && reason.length <= 100) { payload.reason = reason; } // Final validation: ensure all string fields in payload are exactly 100 chars or less // This is a safety check to prevent any encoding or serialization issues const finalPayload = { first_name: payload.first_name.substring(0, 100), last_name: payload.last_name.substring(0, 100), email: payload.email.substring(0, 100), selected_slots: payload.selected_slots, ...(payload.phone && { phone: payload.phone.substring(0, 100) }), ...(payload.reason && { reason: payload.reason.substring(0, 100) }), }; const response = await fetch(API_ENDPOINTS.meetings.createAppointment, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(finalPayload), }); // Read response text first (can only be read once) const responseText = await response.text(); // Check content type before parsing const contentType = response.headers.get("content-type"); let data: any; if (contentType && contentType.includes("application/json")) { try { if (!responseText) { throw new Error(`Server returned empty response (${response.status})`); } data = JSON.parse(responseText); } catch (e) { throw new Error(`Server error (${response.status}): ${response.statusText || 'Invalid response format'}`); } } else { // Response is not JSON (likely HTML error page) // Try to extract error message from HTML if possible let errorMessage = `Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`; // Try to find error details in HTML // Use [\s\S] instead of . with s flag for better compatibility const errorMatch = responseText.match(/]*>([\s\S]*?)<\/pre>/i) || responseText.match(/]*>([\s\S]*?)<\/h1>/i) || responseText.match(/]*>([\s\S]*?)<\/title>/i); if (errorMatch && errorMatch[1]) { const htmlError = errorMatch[1].replace(/<[^>]*>/g, '').trim(); if (htmlError) { errorMessage += `. ${htmlError}`; } } throw new Error(errorMessage); } if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle API response format: { appointment_id, message } // According to API docs, response includes appointment_id and message if (data.appointment_id) { // Construct a minimal Appointment object from the response // We'll use the input data plus the appointment_id from response const appointment: Appointment = { id: data.appointment_id, first_name: input.first_name.trim(), last_name: input.last_name.trim(), email: input.email.trim().toLowerCase(), phone: input.phone?.trim(), reason: input.reason?.trim(), selected_slots: validSlots, status: "pending_review", created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; return appointment; } // Handle different response formats for backward compatibility if (data.appointment) { return data.appointment; } if ((data as any).data) { return (data as any).data; } // If appointment is returned directly return data as unknown as Appointment; } // Get available dates (optional endpoint - may fail if admin hasn't set availability) export async function getAvailableDates(): Promise { try { const response = await fetch(API_ENDPOINTS.meetings.availableDates, { method: "GET", headers: { "Content-Type": "application/json", }, }); // Handle different response formats const contentType = response.headers.get("content-type"); let data: any; if (contentType && contentType.includes("application/json")) { const responseText = await response.text(); if (!responseText) { throw new Error(`Server returned empty response (${response.status})`); } try { data = JSON.parse(responseText); } catch (parseError) { throw new Error(`Invalid response format (${response.status})`); } } else { throw new Error(`Server error (${response.status}): ${response.statusText || 'Invalid response'}`); } if (!response.ok) { // Return empty response instead of throwing - this endpoint is optional return { dates: [], }; } // If API returns array directly, wrap it in response object if (Array.isArray(data)) { return { dates: data, }; } return data as AvailableDatesResponse; } catch (error) { // Return empty response - don't break the app return { dates: [], }; } } // Get weekly availability (Public) export async function getWeeklyAvailability(): Promise { const response = await fetch(API_ENDPOINTS.meetings.weeklyAvailability, { method: "GET", headers: { "Content-Type": "application/json", }, }); const data: any = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle different response formats - API might return array directly or wrapped if (Array.isArray(data)) { return data; } // If wrapped in an object, return as is (our interface supports it) return data; } // Get availability configuration (Public) export async function getAvailabilityConfig(): Promise { const response = await fetch(API_ENDPOINTS.meetings.availabilityConfig, { method: "GET", headers: { "Content-Type": "application/json", }, }); const data: AvailabilityConfig = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; } // Check date availability (Public) export async function checkDateAvailability(date: string): Promise { const response = await fetch(API_ENDPOINTS.meetings.checkDateAvailability, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ date }), }); const data: CheckDateAvailabilityResponse = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; } // Get availability overview (Public) export async function getAvailabilityOverview(): Promise { const response = await fetch(API_ENDPOINTS.meetings.availabilityOverview, { method: "GET", headers: { "Content-Type": "application/json", }, }); const data: AvailabilityOverview = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; } // List appointments (Admin sees all, users see their own) export async function listAppointments(email?: string): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const url = email ? `${API_ENDPOINTS.meetings.listAppointments}?email=${encodeURIComponent(email)}` : API_ENDPOINTS.meetings.listAppointments; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const responseText = await response.text(); if (!response.ok) { let errorData: any; try { errorData = JSON.parse(responseText); } catch { throw new Error(`Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`); } const errorMessage = extractErrorMessage(errorData as unknown as ApiError); throw new Error(errorMessage); } // Parse JSON response let data: any; try { if (!responseText || responseText.trim().length === 0) { return []; } data = JSON.parse(responseText); } catch (error) { throw new Error(`Failed to parse response: Invalid JSON format`); } // Handle different response formats // API returns array directly: [{ id, first_name, ... }, ...] if (Array.isArray(data)) { return data; } // Handle wrapped responses (if any) if (data && typeof data === 'object') { if (data.appointments && Array.isArray(data.appointments)) { return data.appointments; } if (data.results && Array.isArray(data.results)) { return data.results; } // If data is an object but not an array and doesn't have appointments/results, return empty // This shouldn't happen but handle gracefully if (data.id || data.first_name) { // Single appointment object, wrap in array return [data]; } } return []; } // Get user appointments export async function getUserAppointments(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(API_ENDPOINTS.meetings.userAppointments, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle different response formats // API might return array directly or wrapped in an object if (Array.isArray(data)) { return data; } if (data.appointments && Array.isArray(data.appointments)) { return data.appointments; } if (data.results && Array.isArray(data.results)) { return data.results; } return []; } // Get appointment detail export async function getAppointmentDetail(id: string): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: AppointmentResponse = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Schedule appointment (Admin only) export async function scheduleAppointment( id: string, input: ScheduleAppointmentInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/schedule/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(input), }); let data: any; const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { try { const text = await response.text(); data = text ? JSON.parse(text) : {}; } catch (e) { data = {}; } } else { const text = await response.text(); data = text || {}; } if (!response.ok) { // Try to extract detailed error information let errorMessage = `Failed to schedule appointment (${response.status})`; if (data && Object.keys(data).length > 0) { // Check for common error formats if (data.detail) { errorMessage = Array.isArray(data.detail) ? data.detail.join(", ") : String(data.detail); } else if (data.message) { errorMessage = Array.isArray(data.message) ? data.message.join(", ") : String(data.message); } else if (data.error) { errorMessage = Array.isArray(data.error) ? data.error.join(", ") : String(data.error); } else if (typeof data === "string") { errorMessage = data; } else { // Check for field-specific errors const fieldErrors: string[] = []; Object.keys(data).forEach((key) => { if (key !== "detail" && key !== "message" && key !== "error") { const fieldError = data[key]; if (Array.isArray(fieldError)) { fieldErrors.push(`${key}: ${fieldError.join(", ")}`); } else if (typeof fieldError === "string") { fieldErrors.push(`${key}: ${fieldError}`); } } }); if (fieldErrors.length > 0) { errorMessage = fieldErrors.join(". "); } else { // If we have data but can't parse it, show the status errorMessage = `Server error: ${response.status} ${response.statusText}`; } } } else { // No data in response errorMessage = `Server error: ${response.status} ${response.statusText || 'Unknown error'}`; } throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Reject appointment (Admin only) export async function rejectAppointment( id: string, input: RejectAppointmentInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reject/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(input), }); const data: AppointmentResponse = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Get admin availability (public version - uses weekly availability endpoint instead) export async function getPublicAvailability(): Promise { try { // Use weekly availability endpoint which is public const weeklyAvailability = await getWeeklyAvailability(); // Normalize to array format const weekArray = Array.isArray(weeklyAvailability) ? weeklyAvailability : (weeklyAvailability as any).week || []; if (!weekArray || weekArray.length === 0) { return null; } // Convert weekly availability to AdminAvailability format const availabilitySchedule: Record = {}; const availableDays: number[] = []; const availableDaysDisplay: string[] = []; weekArray.forEach((day: any) => { if (day.is_available && day.available_slots && day.available_slots.length > 0) { availabilitySchedule[day.day.toString()] = day.available_slots; availableDays.push(day.day); availableDaysDisplay.push(day.day_name); } }); return { available_days: availableDays, available_days_display: availableDaysDisplay, availability_schedule: availabilitySchedule, all_available_slots: weekArray .filter((d: any) => d.is_available) .flatMap((d: any) => d.available_slots.map((slot: string) => ({ day: d.day, time_slot: slot as "morning" | "afternoon" | "evening" }))), } as AdminAvailability; } catch (error) { return null; } } // Get admin availability export async function getAdminAvailability(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: any = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle new format with availability_schedule // API returns availability_schedule, which may be a JSON string or object // Time slots are strings: "morning", "afternoon", "evening" if (data.availability_schedule) { let availabilitySchedule: Record; // Map numeric indices to string names (in case API returns numeric indices) const numberToTimeSlot: Record = { 0: 'morning', 1: 'afternoon', 2: 'evening', }; // Parse if it's a string, otherwise use as-is let rawSchedule: Record; if (typeof data.availability_schedule === 'string') { try { rawSchedule = JSON.parse(data.availability_schedule); } catch (parseError) { rawSchedule = {}; } } else { rawSchedule = data.availability_schedule; } // Convert to string format, handling both numeric indices and string values availabilitySchedule = {}; Object.keys(rawSchedule).forEach(day => { const slots = rawSchedule[day]; if (Array.isArray(slots) && slots.length > 0) { // Check if slots are numbers (indices) or already strings if (typeof slots[0] === 'number') { // Convert numeric indices to string names availabilitySchedule[day] = (slots as number[]) .map((num: number) => numberToTimeSlot[num]) .filter((slot: string | undefined) => slot !== undefined) as string[]; } else { // Already strings, validate and use as-is availabilitySchedule[day] = (slots as string[]).filter(slot => ['morning', 'afternoon', 'evening'].includes(slot) ); } } }); const availableDays = Object.keys(availabilitySchedule).map(Number); // Generate available_days_display if not provided const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; const availableDaysDisplay = availableDays.map(day => dayNames[day] || `Day ${day}`); return { available_days: availableDays, available_days_display: data.availability_schedule_display ? [data.availability_schedule_display] : availableDaysDisplay, availability_schedule: availabilitySchedule, availability_schedule_display: data.availability_schedule_display, all_available_slots: data.all_available_slots || [], } as AdminAvailability; } // Handle legacy format let availableDays: number[] = []; if (typeof data.available_days === 'string') { try { availableDays = JSON.parse(data.available_days); } catch { // If parsing fails, try splitting by comma availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d)); } } else if (Array.isArray(data.available_days)) { availableDays = data.available_days; } return { available_days: availableDays, available_days_display: data.available_days_display || [], } as AdminAvailability; } // Update admin availability export async function updateAdminAvailability( input: UpdateAvailabilityInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } // Prepare payload using new format (availability_schedule) // API expects availability_schedule as an object with string keys (day numbers) and string arrays (time slot names) // Format: { "0": ["morning", "afternoon"], "1": ["evening"], ... } const payload: any = {}; if (input.availability_schedule) { // Validate and clean the schedule object // API expects: { "0": ["morning", "evening"], "1": ["afternoon"], ... } // Time slots are strings: "morning", "afternoon", "evening" const cleanedSchedule: Record = {}; Object.keys(input.availability_schedule).forEach(key => { // Ensure key is a valid day (0-6) const dayNum = parseInt(key); if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) { return; } const slots = input.availability_schedule[key]; if (Array.isArray(slots) && slots.length > 0) { // Filter to only valid time slot strings and remove duplicates const validSlots = slots .filter((slot: string) => typeof slot === 'string' && ['morning', 'afternoon', 'evening'].includes(slot) ) .filter((slot: string, index: number, self: string[]) => self.indexOf(slot) === index ); // Remove duplicates if (validSlots.length > 0) { // Ensure day key is a string (as per API spec) cleanedSchedule[key.toString()] = validSlots; } } }); if (Object.keys(cleanedSchedule).length === 0) { throw new Error("At least one day with valid time slots must be provided"); } // Sort the schedule keys for consistency const sortedSchedule: Record = {}; Object.keys(cleanedSchedule) .sort((a, b) => parseInt(a) - parseInt(b)) .forEach(key => { sortedSchedule[key] = cleanedSchedule[key]; }); // IMPORTANT: API expects availability_schedule as an object (not stringified) // Format: { "0": ["morning", "afternoon"], "1": ["evening"], ... } payload.availability_schedule = sortedSchedule; } else if (input.available_days) { // Legacy format: available_days payload.available_days = Array.isArray(input.available_days) ? input.available_days.map(day => Number(day)) : input.available_days; } else { throw new Error("Either availability_schedule or available_days must be provided"); } // Try PUT first, fallback to PATCH if needed // The payload object will be JSON stringified, including availability_schedule as an object let response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(payload), }); // If PUT fails with 500, try PATCH (some APIs prefer PATCH for updates) if (!response.ok && response.status === 500) { response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(payload), }); } // Read response text first (can only be read once) const responseText = await response.text(); let data: any; // Get content type const contentType = response.headers.get("content-type") || ""; // Handle empty response if (!responseText || responseText.trim().length === 0) { // If successful status but empty response, refetch the availability if (response.ok) { return await getAdminAvailability(); } throw new Error(`Server error (${response.status}): ${response.statusText || 'Empty response from server'}`); } // Try to parse as JSON if (contentType.includes("application/json")) { try { data = JSON.parse(responseText); } catch (parseError) { throw new Error(`Server error (${response.status}): Invalid JSON response format`); } } else { // Response is not JSON - try to extract useful information // Try to extract error message from HTML if it's an HTML error page let errorMessage = `Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`; let actualError = ''; let errorType = ''; let fullTraceback = ''; if (responseText) { // Extract Django error details from HTML const titleMatch = responseText.match(/]*>(.*?)<\/title>/i); const h1Match = responseText.match(/]*>(.*?)<\/h1>/i); // Try to find the actual error traceback in
 tags (Django debug pages)
      const tracebackMatch = responseText.match(/]*class="[^"]*traceback[^"]*"[^>]*>([\s\S]*?)<\/pre>/i) ||
                            responseText.match(/]*>([\s\S]*?)<\/pre>/i);
      
      // Extract the actual error type and message
      const errorTypeMatch = responseText.match(/]*>(.*?)<\/h2>/i);
      
      if (tracebackMatch && tracebackMatch[1]) {
        // Extract the full traceback and find the actual error
        const tracebackText = tracebackMatch[1].replace(/<[^>]*>/g, ''); // Remove HTML tags
        fullTraceback = tracebackText;
        const tracebackLines = tracebackText.split('\n').filter(line => line.trim());
        
        // Look for the actual error message - Django errors usually appear at the end
        // First, try to find error patterns in the entire traceback
        const errorPatterns = [
          // Database column errors
          /column\s+[\w.]+\.(\w+)\s+(does not exist|already exists|is missing)/i,
          // Programming errors
          /(ProgrammingError|OperationalError|IntegrityError|DatabaseError|ValueError|TypeError|AttributeError|KeyError):\s*(.+?)(?:\n|$)/i,
          // Generic error patterns
          /^(\w+Error):\s*(.+)$/i,
          // Error messages without type
          /^(.+Error[:\s]+.+)$/i,
        ];
        
        // Search from the end backwards (errors are usually at the end)
        for (let i = tracebackLines.length - 1; i >= 0; i--) {
          const line = tracebackLines[i];
          
          // Check each pattern
          for (const pattern of errorPatterns) {
            const match = line.match(pattern);
            if (match) {
              // For database column errors, capture the full message
              if (pattern.source.includes('column')) {
                actualError = match[0];
                errorType = 'DatabaseError';
              } else if (match[1] && match[2]) {
                errorType = match[1];
                actualError = match[2].trim();
              } else {
                actualError = match[0];
              }
              break;
            }
          }
          if (actualError) break;
        }
        
        // If no pattern match, look for lines containing "Error" or common error keywords
        if (!actualError) {
          for (let i = tracebackLines.length - 1; i >= Math.max(0, tracebackLines.length - 10); i--) {
            const line = tracebackLines[i];
            if (line.match(/(Error|Exception|Failed|Invalid|Missing|does not exist|already exists)/i)) {
              actualError = line;
              break;
            }
          }
        }
        
        // Last resort: get the last line
        if (!actualError && tracebackLines.length > 0) {
          actualError = tracebackLines[tracebackLines.length - 1];
        }
        
        // Clean up the error message
        if (actualError) {
          actualError = actualError.trim();
          // Remove common prefixes
          actualError = actualError.replace(/^(Traceback|File|Error|Exception):\s*/i, '');
        }
      } else if (errorTypeMatch && errorTypeMatch[1]) {
        errorType = errorTypeMatch[1].replace(/<[^>]*>/g, '').trim();
        actualError = errorType;
        if (errorType && errorType.length < 200) {
          errorMessage += `. ${errorType}`;
        }
      } else if (h1Match && h1Match[1]) {
        actualError = h1Match[1].replace(/<[^>]*>/g, '').trim();
        if (actualError && actualError.length < 200) {
          errorMessage += `. ${actualError}`;
        }
      } else if (titleMatch && titleMatch[1]) {
        actualError = titleMatch[1].replace(/<[^>]*>/g, '').trim();
        if (actualError && actualError.length < 200) {
          errorMessage += `. ${actualError}`;
        }
      }
    }
    
    // Update error message with the extracted error
    if (actualError) {
      errorMessage = `Server error (${response.status}): ${actualError}`;
    }
    
    throw new Error(errorMessage);
  }

  if (!response.ok) {
    const errorMessage = extractErrorMessage(data as unknown as ApiError);
    
    // Build detailed error message
    let detailedError = `Server error (${response.status}): `;
    if (data && typeof data === 'object') {
      if (data.detail) {
        detailedError += Array.isArray(data.detail) ? data.detail.join(", ") : String(data.detail);
      } else if (data.error) {
        detailedError += Array.isArray(data.error) ? data.error.join(", ") : String(data.error);
      } else if (data.message) {
        detailedError += Array.isArray(data.message) ? data.message.join(", ") : String(data.message);
      } else {
        detailedError += response.statusText || 'Failed to update availability';
      }
    } else if (responseText && responseText.length > 0) {
      // Try to extract error from HTML response if it's not JSON
      detailedError += responseText.substring(0, 200);
    } else {
      detailedError += response.statusText || 'Failed to update availability';
    }
    
    throw new Error(detailedError);
  }

  // Handle new format with availability_schedule in response
  // API returns availability_schedule, which may be a JSON string or object
  // Time slots may be strings or numeric indices
  if (data && data.availability_schedule) {
    let availabilitySchedule: Record;
    
    // Map numeric indices to string names (in case API returns numeric indices)
    const numberToTimeSlot: Record = {
      0: 'morning',
      1: 'afternoon',
      2: 'evening',
    };
    
    // Parse if it's a string, otherwise use as-is
    let rawSchedule: Record;
    if (typeof data.availability_schedule === 'string') {
      try {
        rawSchedule = JSON.parse(data.availability_schedule);
      } catch (parseError) {
        rawSchedule = {};
      }
    } else if (typeof data.availability_schedule === 'object') {
      rawSchedule = data.availability_schedule;
    } else {
      rawSchedule = {};
    }
    
    // Convert to string format, handling both numeric indices and string values
    availabilitySchedule = {};
    Object.keys(rawSchedule).forEach(day => {
      const slots = rawSchedule[day];
      if (Array.isArray(slots) && slots.length > 0) {
        // Check if slots are numbers (indices) or already strings
        if (typeof slots[0] === 'number') {
          // Convert numeric indices to string names
          availabilitySchedule[day] = (slots as number[])
            .map((num: number) => numberToTimeSlot[num])
            .filter((slot: string | undefined) => slot !== undefined) as string[];
        } else {
          // Already strings, validate and use as-is
          availabilitySchedule[day] = (slots as string[]).filter(slot => 
            ['morning', 'afternoon', 'evening'].includes(slot)
          );
        }
      }
    });
    
    const availableDays = Object.keys(availabilitySchedule).map(Number);
    const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
    const availableDaysDisplay = availableDays.map(day => dayNames[day] || `Day ${day}`);

    return {
      available_days: availableDays,
      available_days_display: data.availability_schedule_display ? 
        (Array.isArray(data.availability_schedule_display) ? 
          data.availability_schedule_display : 
          [data.availability_schedule_display]) : 
        availableDaysDisplay,
      availability_schedule: availabilitySchedule,
      availability_schedule_display: data.availability_schedule_display,
      all_available_slots: data.all_available_slots || [],
    } as AdminAvailability;
  }
  
  // If response is empty but successful (200), return empty availability
  // This might happen if the server doesn't return data on success
  if (response.ok && (!data || Object.keys(data).length === 0)) {
    // Refetch the availability to get the updated data
    return getAdminAvailability();
  }

  // Handle legacy format
  let availableDays: number[] = [];
  if (typeof data.available_days === 'string') {
    try {
      availableDays = JSON.parse(data.available_days);
    } catch {
      // If parsing fails, try splitting by comma
      availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d));
    }
  } else if (Array.isArray(data.available_days)) {
    availableDays = data.available_days;
  }

  return {
    available_days: availableDays,
    available_days_display: data.available_days_display || [],
  } as AdminAvailability;
}

// Get appointment stats (Admin only)
export async function getAppointmentStats(): Promise {
  const tokens = getStoredTokens();
  
  if (!tokens.access) {
    throw new Error("Authentication required.");
  }

  const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}stats/`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${tokens.access}`,
    },
  });

  const data: AppointmentStats = await response.json();

  if (!response.ok) {
    const errorMessage = extractErrorMessage(data as unknown as ApiError);
    throw new Error(errorMessage);
  }

  return data;
}

// Get user appointment stats
export async function getUserAppointmentStats(email: string): Promise {
  const tokens = getStoredTokens();
  
  if (!tokens.access) {
    throw new Error("Authentication required.");
  }

  if (!email) {
    throw new Error("Email is required to fetch user appointment stats.");
  }

  const response = await fetch(API_ENDPOINTS.meetings.userAppointmentStats, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${tokens.access}`,
    },
    body: JSON.stringify({ email }),
  });

  const responseText = await response.text();

  if (!response.ok) {
    let errorData: any;
    try {
      errorData = JSON.parse(responseText);
    } catch {
      throw new Error(`Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`);
    }
    const errorMessage = extractErrorMessage(errorData as unknown as ApiError);
    throw new Error(errorMessage);
  }

  let data: UserAppointmentStats;
  try {
    if (!responseText || responseText.trim().length === 0) {
      throw new Error("Empty response from server");
    }
    data = JSON.parse(responseText);
  } catch (error) {
    throw new Error(`Failed to parse response: Invalid JSON format`);
  }

  return data;
}

// Get Jitsi meeting info
export async function getJitsiMeetingInfo(id: string): Promise {
  const tokens = getStoredTokens();
  
  if (!tokens.access) {
    throw new Error("Authentication required.");
  }

  const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/jitsi-meeting/`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${tokens.access}`,
    },
  });

  const data: JitsiMeetingInfo = await response.json();

  if (!response.ok) {
    const errorMessage = extractErrorMessage(data as unknown as ApiError);
    throw new Error(errorMessage);
  }

  return data;
}