import type {
  Appointment,
  AppointmentActivityEntry,
  AvailabilityResponse,
  DaySlots,
  DoctorCard,
  Specialty,
} from './appointments-types';

export class AppointmentsClientError extends Error {
  constructor(
    message: string,
    readonly statusCode: number,
  ) {
    super(message);
    this.name = 'AppointmentsClientError';
  }
}

async function handle<T>(res: Response): Promise<T> {
  if (res.status === 204) return undefined as T;
  const data = (await res.json().catch(() => null)) as
    | T
    | { message: string | string[] }
    | null;
  if (!res.ok) {
    const msg =
      (data as { message?: string | string[] } | null)?.message ??
      `Request failed (${res.status})`;
    throw new AppointmentsClientError(
      Array.isArray(msg) ? msg.join('\n') : msg,
      res.status,
    );
  }
  return data as T;
}

export const appointmentsClient = {
  async listSpecialties(): Promise<Specialty[]> {
    const res = await fetch('/api/doctors/specialties', { cache: 'no-store' });
    return handle<Specialty[]>(res);
  },

  async browseDoctors(params: {
    specialty?: string;
    query?: string;
  }): Promise<DoctorCard[]> {
    const search = new URLSearchParams();
    if (params.specialty) search.set('specialty', params.specialty);
    if (params.query) search.set('q', params.query);
    const qs = search.toString();
    const res = await fetch(`/api/doctors${qs ? `?${qs}` : ''}`, { cache: 'no-store' });
    return handle<DoctorCard[]>(res);
  },

  async getDoctor(id: string): Promise<DoctorCard> {
    const res = await fetch(`/api/doctors/${id}`, { cache: 'no-store' });
    return handle<DoctorCard>(res);
  },

  async getAvailability(doctorId: string, days = 7, patientTimezone?: string): Promise<AvailabilityResponse> {
    const tz = patientTimezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
    const res = await fetch(
      `/api/appointments/availability/${doctorId}?days=${days}&patientTimezone=${encodeURIComponent(tz)}`,
      { cache: 'no-store' },
    );
    return handle<AvailabilityResponse>(res);
  },

  async book(payload: {
    doctorId: string;
    specialtyName: string;
    scheduledAt: string;
    symptoms?: string;
    urgency?: 'routine' | 'urgent' | 'emergency';
    rescheduledFromAppointmentId?: string;
  }): Promise<Appointment> {
    const res = await fetch('/api/appointments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    return handle<Appointment>(res);
  },

  /**
   * Guided-routing booking — no doctor selection. The medical board (admins)
   * pick a doctor based on the symptoms + AI recommendation. Returns the
   * created appointment with `awaitingAdminApproval=true`.
   */
  async bookGuided(payload: {
    specialtyName?: string;
    scheduledAt: string;
    timezone?: string;
    symptoms: string;
    urgency?: 'routine' | 'urgent' | 'emergency';
    familyMemberId?: string;
    voiceNoteUrl?: string;
    voiceNoteDurationSeconds?: number;
    voiceNoteMimeType?: string;
  }): Promise<Appointment> {
    const res = await fetch('/api/appointments/guided', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    return handle<Appointment>(res);
  },

  async getIntroBlock(): Promise<{
    enabled: boolean;
    title: string;
    intro: string | null;
    bullets: string[];
    updatedAt: string;
  }> {
    const res = await fetch('/api/appointments/intro-block', { cache: 'no-store' });
    return handle(res);
  },

  /**
   * Send an audio blob to Whisper and get text back. Audio is discarded
   * server-side after transcription.
   */
  async transcribeAudio(blob: Blob, filename = 'voice.webm'): Promise<{
    text: string;
    language: string | null;
  }> {
    const fd = new FormData();
    fd.append('file', blob, filename);
    const res = await fetch('/api/appointments/transcribe', {
      method: 'POST',
      body: fd,
    });
    return handle(res);
  },

  /**
   * Upload an optional voice note to attach to a guided booking. Returns
   * the public URL the caller passes to `bookGuided`.
   */
  async uploadVoiceNote(
    blob: Blob,
    filename = 'voice-note.webm',
    durationSeconds?: number,
  ): Promise<{
    url: string;
    mimeType: string;
    sizeBytes: number;
    durationSeconds: number | null;
  }> {
    const fd = new FormData();
    fd.append('file', blob, filename);
    if (durationSeconds !== undefined) {
      fd.append('durationSeconds', String(Math.round(durationSeconds)));
    }
    const res = await fetch('/api/appointments/voice-note', {
      method: 'POST',
      body: fd,
    });
    return handle(res);
  },

  async listMine(): Promise<Appointment[]> {
    const res = await fetch('/api/appointments', { cache: 'no-store' });
    return handle<Appointment[]>(res);
  },

  async getOne(id: string): Promise<Appointment> {
    const res = await fetch(`/api/appointments/${id}`, { cache: 'no-store' });
    return handle<Appointment>(res);
  },

  async cancel(id: string, reason?: string): Promise<Appointment> {
    const res = await fetch(`/api/appointments/${id}/cancel`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason }),
    });
    return handle<Appointment>(res);
  },

  async reportDoctorNoShow(id: string): Promise<Appointment> {
    const res = await fetch(`/api/appointments/${id}/report-doctor-no-show`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
    });
    return handle<Appointment>(res);
  },

  async requestReschedule(id: string): Promise<Appointment> {
    const res = await fetch(`/api/appointments/${id}/reschedule-request`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
    });
    return handle<Appointment>(res);
  },

  async getActivity(id: string): Promise<AppointmentActivityEntry[]> {
    const res = await fetch(`/api/appointments/${id}/activity`, {
      cache: 'no-store',
    });
    return handle<AppointmentActivityEntry[]>(res);
  },
};
