export interface Specialty {
  id: string;
  name: string;
  icon: string;
  description: string;
}

export interface DoctorCard {
  id: string;
  userId: string;
  fullName: string;
  title: string;
  primarySpecialty: string | null;
  specialties: string[];
  experience: number;
  consultationFee: number;
  feeCurrency: string;
  ratingAvg: number;
  ratingCount: number;
  languages: string[];
  profileImageUrl: string | null;
  about: string | null;
  slotDurationMinutes: number;
  qualifications?: Array<{ degree: string; institution: string; year: number }>;
}

export interface SlotWindow {
  startAt: string;
  endAt: string;
  durationMinutes: number;
  available: boolean;
}

export interface DaySlots {
  date: string;
  dayOfWeek: number;
  slots: SlotWindow[];
}

export interface AvailabilityResponse {
  doctorTimezone: string;
  patientTimezone: string;
  slots: DaySlots[];
}

export type AppointmentStatus =
  | 'pending'
  | 'allocated'
  | 'confirmed'
  | 'in_progress'
  | 'completed'
  | 'cancelled'
  | 'no_show';

export type AppointmentNoShowBy = 'patient' | 'doctor' | 'both';

export interface Appointment {
  id: string;
  patientId: string;
  doctorId: string;
  specialtyName: string;
  scheduledAt: string;
  status: AppointmentStatus;
  /** Patient's IANA timezone at booking time. */
  timezone: string;
  /** Doctor's IANA timezone, captured from their schedule at booking time. */
  doctorTimezone?: string | null;
  symptoms?: string | null;
  familyMemberId?: string | null;
  cancelReason?: string | null;
  cancelledBy?: string | null;
  completedAt?: string | null;
  durationMinutes?: number;
  /** Who missed the appointment, if any. */
  noShowBy?: AppointmentNoShowBy | null;
  /** One-shot timestamp set when patient asks for the free reschedule. */
  rescheduleRequestedAt?: string | null;
  /** Link from a replacement appointment back to the original. */
  rescheduledFromAppointmentId?: string | null;
  /** Has the consultation credit been returned for this appointment? */
  creditRefunded?: boolean;
  /** True until an admin commits a doctor allocation for guided-routed bookings. */
  awaitingAdminApproval?: boolean;
  /** How the doctor was chosen (set after allocation). */
  assignmentMethod?: 'patient_direct' | 'ai_auto' | 'admin_manual' | null;
  /** Optional 60-second voice note attached at booking time. Path under /api/v2/uploads/voice-notes/. */
  voiceNoteUrl?: string | null;
  voiceNoteDurationSeconds?: number | null;
  voiceNoteMimeType?: string | null;
  createdAt?: string;
  updatedAt?: string;
  /** Counterparty display name — joined from users table by the backend. */
  doctorName?: string | null;
  doctorTitle?: string | null;
  patientName?: string | null;
  /**
   * Human-readable appointment number, e.g. "APT-2026-000007". Stable for the
   * lifetime of the row and safe to read aloud over the phone. Backed by a
   * Postgres sequence + BEFORE INSERT trigger on the backend so concurrent
   * inserts can't collide.
   */
  displayNumber?: string | null;
}

export interface AppointmentActivityEntry {
  id: string;
  action: string;
  actorId: string | null;
  createdAt: string;
  metadata: Record<string, unknown> | null;
}

/**
 * Convert a backend specialty slug ("cardiology", "mental_health") into a
 * patient-facing label ("Cardiology", "Mental Health"). Title-case +
 * underscores-to-spaces is the simplest correct transformation; if we ever
 * want localised labels we'll bring in a catalog.
 */
export function humanizeSpecialty(slug: string | null | undefined): string {
  if (!slug) return '';
  return slug
    .replace(/_/g, ' ')
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

/**
 * Best-effort doctor display label. Falls back to specialty if the backend
 * didn't supply the join — keeps the card useful even on legacy rows.
 *
 * `doctorTitle` in the doctors table is a free-text credential string
 * (e.g. "MBBS, FCPS"), not an honorific. Always render "Dr." up front so
 * the row reads naturally — credentials are surfaced separately via
 * {@link appointmentDoctorCredentials}.
 */
export function appointmentDoctorLabel(a: Appointment): string {
  if (a.doctorName) {
    const name = a.doctorName.trim();
    return /^(dr\.?|prof\.?|mr\.?|ms\.?|mrs\.?)\s/i.test(name)
      ? name
      : `Dr. ${name}`;
  }
  // Guided-flow appointments hide the doctor until the admin approves — show
  // a friendly placeholder so the patient sees status, not a blank.
  if (a.awaitingAdminApproval) {
    return 'Awaiting doctor assignment';
  }
  return humanizeSpecialty(a.specialtyName) || 'Consultation';
}

/** True for guided bookings still in the admin queue (no doctor visible yet). */
export function isAwaitingAssignment(a: Appointment): boolean {
  return !!a.awaitingAdminApproval && !a.doctorId;
}

export function appointmentDoctorCredentials(a: Appointment): string | null {
  const t = a.doctorTitle?.trim();
  if (!t) return null;
  if (/^(dr\.?|prof\.?|mr\.?|ms\.?|mrs\.?)\s?$/i.test(t)) return null;
  return t;
}

export function isAppointmentMissed(a: Appointment): boolean {
  if (a.status === 'cancelled' || a.status === 'completed' || a.status === 'no_show') return false;
  return new Date(a.scheduledAt).getTime() < Date.now();
}

/**
 * Render the appointment moment in a specific timezone. Used to show both
 * sides ("You: …, Doctor: …") so neither party guesses the wall-clock for
 * the other. Falls back to the browser local rendering if `timezone` is
 * empty or invalid.
 */
export function formatInTimezone(
  iso: string,
  timezone: string | null | undefined,
): { date: string; time: string; tz: string } {
  const dt = new Date(iso);
  if (Number.isNaN(dt.getTime())) {
    return { date: '—', time: '—', tz: timezone ?? '' };
  }
  const opts: Intl.DateTimeFormatOptions = {
    timeZone: timezone || undefined,
    timeZoneName: 'short',
  };
  try {
    const date = dt.toLocaleDateString(undefined, {
      ...opts,
      weekday: 'short',
      day: 'numeric',
      month: 'short',
      year: 'numeric',
      timeZoneName: undefined,
    });
    const timeParts = new Intl.DateTimeFormat(undefined, {
      ...opts,
      hour: 'numeric',
      minute: '2-digit',
    }).formatToParts(dt);
    const time = timeParts
      .filter((p) => p.type !== 'timeZoneName')
      .map((p) => p.value)
      .join('')
      .trim();
    const tzPart = timeParts.find((p) => p.type === 'timeZoneName');
    return {
      date,
      time,
      tz: tzPart?.value ?? timezone ?? '',
    };
  } catch {
    return {
      date: dt.toLocaleDateString(),
      time: dt.toLocaleTimeString(undefined, {
        hour: 'numeric',
        minute: '2-digit',
      }),
      tz: timezone ?? '',
    };
  }
}

export const APPOINTMENT_STATUS_LABEL: Record<AppointmentStatus, string> = {
  pending: 'Pending',
  allocated: 'Allocated',
  confirmed: 'Confirmed',
  in_progress: 'In progress',
  completed: 'Completed',
  cancelled: 'Cancelled',
  no_show: 'No show',
};
